From ec89961e072bb7e5ecdec12acf758477e9deb73d Mon Sep 17 00:00:00 2001 From: Niels Date: Fri, 2 Aug 2024 14:18:38 +0200 Subject: [PATCH] feat: refactor --- .gitignore | 11 +- packages/chat_repository_interface/.gitignore | 29 + .../chat_repository_interface/CHANGELOG.md | 3 + packages/chat_repository_interface/LICENSE | 1 + packages/chat_repository_interface/README.md | 39 ++ .../analysis_options.yaml | 4 + .../lib/chat_repository_interface.dart | 17 + .../interfaces/chat_repostory_interface.dart | 55 ++ .../interfaces/user_repository_interface.dart | 7 + .../lib/src/local/local_chat_repository.dart | 214 ++++++ .../lib/src/local/local_user_repository.dart | 52 ++ .../lib/src/models/chat_model.dart | 65 ++ .../lib/src/models/message_model.dart | 37 + .../lib/src/models/user_model.dart | 31 + .../lib/src/services/chat_service.dart | 176 +++++ .../chat_repository_interface/pubspec.yaml | 57 ++ packages/firebase_chat_repository/.gitignore | 29 + .../firebase_chat_repository/CHANGELOG.md | 3 + packages/firebase_chat_repository/LICENSE | 1 + packages/firebase_chat_repository/README.md | 39 ++ .../analysis_options.yaml | 4 + .../lib/firebase_chat_repository.dart | 7 + .../firebase_chat_repository/pubspec.yaml | 54 ++ packages/flutter_chat/.gitignore | 29 + .../CHANGELOG.md~72104f3 (feat: refactor) | 3 + .../LICENSE~72104f3 (feat: refactor) | 1 + .../README.md~72104f3 (feat: refactor) | 39 ++ packages/flutter_chat/analysis_options.yaml | 11 +- packages/flutter_chat/example/.gitignore | 9 - .../example/analysis_options.yaml | 29 +- packages/flutter_chat/example/lib/main.dart | 10 +- packages/flutter_chat/example/pubspec.yaml | 85 ++- .../example/test/widget_test.dart | 14 - packages/flutter_chat/lib/flutter_chat.dart | 19 +- .../lib/src/config/chat_builders.dart | 98 +++ .../lib/src/config/chat_options.dart | 28 + .../lib/src/config/chat_translations.dart | 16 +- ...et.dart => flutter_chat_entry_widget.dart} | 31 +- .../src/flutter_chat_navigator_userstory.dart | 537 ++++++-------- .../lib/src/flutter_chat_userstory.dart | 325 --------- packages/flutter_chat/lib/src/go_router.dart | 40 -- .../lib/src/models/chat_configuration.dart | 143 ---- packages/flutter_chat/lib/src/routes.dart | 22 - .../lib/src/screens/chat_detail_screen.dart | 661 ++++++++++++++++++ .../lib/src/screens/chat_profile_screen.dart | 275 ++++++++ .../lib/src/screens/chat_screen.dart | 588 ++++++++++++++++ .../src/screens/creation/new_chat_screen.dart | 240 +++++++ .../creation/new_group_chat_overview.dart | 395 +++++++++++ .../creation/new_group_chat_screen.dart | 283 ++++++++ .../creation/widgets/image_picker.dart | 78 +++ .../creation/widgets/search_field.dart | 46 ++ .../screens/creation/widgets/search_icon.dart | 24 + .../screens/creation/widgets/user_list.dart | 182 +++++ .../lib/src/services/date_formatter.dart | 2 +- packages/flutter_chat/pubspec.yaml | 80 ++- .../analysis_options.yaml | 9 - .../lib/config/firebase_chat_options.dart | 60 -- .../lib/dto/firebase_chat_document.dart | 77 -- .../lib/dto/firebase_message_document.dart | 49 -- .../lib/dto/firebase_user_document.dart | 50 -- .../lib/flutter_chat_firebase.dart | 7 - .../service/firebase_chat_detail_service.dart | 346 --------- .../firebase_chat_overview_service.dart | 537 -------------- .../lib/service/firebase_chat_service.dart | 85 --- .../service/firebase_chat_user_service.dart | 106 --- .../lib/service/service.dart | 4 - packages/flutter_chat_firebase/pubspec.yaml | 33 - .../analysis_options.yaml | 9 - .../lib/flutter_chat_interface.dart | 9 - .../lib/src/chat_data_provider.dart | 19 - .../lib/src/model/chat.dart | 77 -- .../lib/src/model/chat_image_message.dart | 48 -- .../lib/src/model/chat_message.dart | 31 - .../lib/src/model/chat_text_message.dart | 41 -- .../lib/src/model/chat_user.dart | 72 -- .../lib/src/model/group_chat.dart | 117 ---- .../lib/src/model/model.dart | 7 - .../lib/src/model/personal_chat.dart | 93 --- .../lib/src/service/chat_detail_service.dart | 34 - .../src/service/chat_overview_service.dart | 42 -- .../lib/src/service/chat_service.dart | 14 - .../lib/src/service/service.dart | 4 - .../lib/src/service/user_service.dart | 13 - packages/flutter_chat_interface/pubspec.yaml | 27 - .../flutter_chat_local/analysis_options.yaml | 9 - .../lib/local_chat_service.dart | 7 - .../service/local_chat_detail_service.dart | 133 ---- .../service/local_chat_overview_service.dart | 116 --- .../lib/service/local_chat_service.dart | 46 -- .../lib/service/local_chat_user_service.dart | 40 -- packages/flutter_chat_local/pubspec.yaml | 25 - .../flutter_chat_view/analysis_options.yaml | 9 - .../lib/flutter_chat_view.dart | 18 - .../lib/src/components/chat_bottom.dart | 113 --- .../lib/src/components/chat_detail_row.dart | 174 ----- .../lib/src/components/chat_image.dart | 44 -- .../lib/src/components/chat_row.dart | 119 ---- .../components/image_loading_snackbar.dart | 19 - .../src/components/image_picker_popup.dart | 33 - .../lib/src/config/chat_options.dart | 334 --------- .../lib/src/config/chat_text_styles.dart | 13 - .../lib/src/screens/chat_detail_screen.dart | 269 ------- .../lib/src/screens/chat_profile_screen.dart | 219 ------ .../lib/src/screens/chat_screen.dart | 378 ---------- .../lib/src/screens/new_chat_screen.dart | 228 ------ .../new_group_chat_overview_screen.dart | 319 --------- .../src/screens/new_group_chat_screen.dart | 300 -------- .../lib/src/services/profile_service.dart | 34 - packages/flutter_chat_view/pubspec.yaml | 38 - .../test/flutter_community_chat_test.dart | 12 - 110 files changed, 4298 insertions(+), 6049 deletions(-) create mode 100644 packages/chat_repository_interface/.gitignore create mode 100644 packages/chat_repository_interface/CHANGELOG.md create mode 100644 packages/chat_repository_interface/LICENSE create mode 100644 packages/chat_repository_interface/README.md create mode 100644 packages/chat_repository_interface/analysis_options.yaml create mode 100644 packages/chat_repository_interface/lib/chat_repository_interface.dart create mode 100644 packages/chat_repository_interface/lib/src/interfaces/chat_repostory_interface.dart create mode 100644 packages/chat_repository_interface/lib/src/interfaces/user_repository_interface.dart create mode 100644 packages/chat_repository_interface/lib/src/local/local_chat_repository.dart create mode 100644 packages/chat_repository_interface/lib/src/local/local_user_repository.dart create mode 100644 packages/chat_repository_interface/lib/src/models/chat_model.dart create mode 100644 packages/chat_repository_interface/lib/src/models/message_model.dart create mode 100644 packages/chat_repository_interface/lib/src/models/user_model.dart create mode 100644 packages/chat_repository_interface/lib/src/services/chat_service.dart create mode 100644 packages/chat_repository_interface/pubspec.yaml create mode 100644 packages/firebase_chat_repository/.gitignore create mode 100644 packages/firebase_chat_repository/CHANGELOG.md create mode 100644 packages/firebase_chat_repository/LICENSE create mode 100644 packages/firebase_chat_repository/README.md create mode 100644 packages/firebase_chat_repository/analysis_options.yaml create mode 100644 packages/firebase_chat_repository/lib/firebase_chat_repository.dart create mode 100644 packages/firebase_chat_repository/pubspec.yaml create mode 100644 packages/flutter_chat/.gitignore create mode 100644 packages/flutter_chat/CHANGELOG.md~72104f3 (feat: refactor) create mode 100644 packages/flutter_chat/LICENSE~72104f3 (feat: refactor) create mode 100644 packages/flutter_chat/README.md~72104f3 (feat: refactor) delete mode 100644 packages/flutter_chat/example/test/widget_test.dart create mode 100644 packages/flutter_chat/lib/src/config/chat_builders.dart create mode 100644 packages/flutter_chat/lib/src/config/chat_options.dart rename packages/{flutter_chat_view => flutter_chat}/lib/src/config/chat_translations.dart (96%) rename packages/flutter_chat/lib/src/{chat_entry_widget.dart => flutter_chat_entry_widget.dart} (85%) delete mode 100644 packages/flutter_chat/lib/src/flutter_chat_userstory.dart delete mode 100644 packages/flutter_chat/lib/src/go_router.dart delete mode 100644 packages/flutter_chat/lib/src/models/chat_configuration.dart delete mode 100644 packages/flutter_chat/lib/src/routes.dart create mode 100644 packages/flutter_chat/lib/src/screens/chat_detail_screen.dart create mode 100644 packages/flutter_chat/lib/src/screens/chat_profile_screen.dart create mode 100644 packages/flutter_chat/lib/src/screens/chat_screen.dart create mode 100644 packages/flutter_chat/lib/src/screens/creation/new_chat_screen.dart create mode 100644 packages/flutter_chat/lib/src/screens/creation/new_group_chat_overview.dart create mode 100644 packages/flutter_chat/lib/src/screens/creation/new_group_chat_screen.dart create mode 100644 packages/flutter_chat/lib/src/screens/creation/widgets/image_picker.dart create mode 100644 packages/flutter_chat/lib/src/screens/creation/widgets/search_field.dart create mode 100644 packages/flutter_chat/lib/src/screens/creation/widgets/search_icon.dart create mode 100644 packages/flutter_chat/lib/src/screens/creation/widgets/user_list.dart rename packages/{flutter_chat_view => flutter_chat}/lib/src/services/date_formatter.dart (96%) delete mode 100644 packages/flutter_chat_firebase/analysis_options.yaml delete mode 100644 packages/flutter_chat_firebase/lib/config/firebase_chat_options.dart delete mode 100644 packages/flutter_chat_firebase/lib/dto/firebase_chat_document.dart delete mode 100644 packages/flutter_chat_firebase/lib/dto/firebase_message_document.dart delete mode 100644 packages/flutter_chat_firebase/lib/dto/firebase_user_document.dart delete mode 100644 packages/flutter_chat_firebase/lib/flutter_chat_firebase.dart delete mode 100644 packages/flutter_chat_firebase/lib/service/firebase_chat_detail_service.dart delete mode 100644 packages/flutter_chat_firebase/lib/service/firebase_chat_overview_service.dart delete mode 100644 packages/flutter_chat_firebase/lib/service/firebase_chat_service.dart delete mode 100644 packages/flutter_chat_firebase/lib/service/firebase_chat_user_service.dart delete mode 100644 packages/flutter_chat_firebase/lib/service/service.dart delete mode 100644 packages/flutter_chat_firebase/pubspec.yaml delete mode 100644 packages/flutter_chat_interface/analysis_options.yaml delete mode 100644 packages/flutter_chat_interface/lib/flutter_chat_interface.dart delete mode 100644 packages/flutter_chat_interface/lib/src/chat_data_provider.dart delete mode 100644 packages/flutter_chat_interface/lib/src/model/chat.dart delete mode 100644 packages/flutter_chat_interface/lib/src/model/chat_image_message.dart delete mode 100644 packages/flutter_chat_interface/lib/src/model/chat_message.dart delete mode 100644 packages/flutter_chat_interface/lib/src/model/chat_text_message.dart delete mode 100644 packages/flutter_chat_interface/lib/src/model/chat_user.dart delete mode 100644 packages/flutter_chat_interface/lib/src/model/group_chat.dart delete mode 100644 packages/flutter_chat_interface/lib/src/model/model.dart delete mode 100644 packages/flutter_chat_interface/lib/src/model/personal_chat.dart delete mode 100644 packages/flutter_chat_interface/lib/src/service/chat_detail_service.dart delete mode 100644 packages/flutter_chat_interface/lib/src/service/chat_overview_service.dart delete mode 100644 packages/flutter_chat_interface/lib/src/service/chat_service.dart delete mode 100644 packages/flutter_chat_interface/lib/src/service/service.dart delete mode 100644 packages/flutter_chat_interface/lib/src/service/user_service.dart delete mode 100644 packages/flutter_chat_interface/pubspec.yaml delete mode 100644 packages/flutter_chat_local/analysis_options.yaml delete mode 100644 packages/flutter_chat_local/lib/local_chat_service.dart delete mode 100644 packages/flutter_chat_local/lib/service/local_chat_detail_service.dart delete mode 100644 packages/flutter_chat_local/lib/service/local_chat_overview_service.dart delete mode 100644 packages/flutter_chat_local/lib/service/local_chat_service.dart delete mode 100644 packages/flutter_chat_local/lib/service/local_chat_user_service.dart delete mode 100644 packages/flutter_chat_local/pubspec.yaml delete mode 100644 packages/flutter_chat_view/analysis_options.yaml delete mode 100644 packages/flutter_chat_view/lib/flutter_chat_view.dart delete mode 100644 packages/flutter_chat_view/lib/src/components/chat_bottom.dart delete mode 100644 packages/flutter_chat_view/lib/src/components/chat_detail_row.dart delete mode 100644 packages/flutter_chat_view/lib/src/components/chat_image.dart delete mode 100644 packages/flutter_chat_view/lib/src/components/chat_row.dart delete mode 100644 packages/flutter_chat_view/lib/src/components/image_loading_snackbar.dart delete mode 100644 packages/flutter_chat_view/lib/src/components/image_picker_popup.dart delete mode 100644 packages/flutter_chat_view/lib/src/config/chat_options.dart delete mode 100644 packages/flutter_chat_view/lib/src/config/chat_text_styles.dart delete mode 100644 packages/flutter_chat_view/lib/src/screens/chat_detail_screen.dart delete mode 100644 packages/flutter_chat_view/lib/src/screens/chat_profile_screen.dart delete mode 100644 packages/flutter_chat_view/lib/src/screens/chat_screen.dart delete mode 100644 packages/flutter_chat_view/lib/src/screens/new_chat_screen.dart delete mode 100644 packages/flutter_chat_view/lib/src/screens/new_group_chat_overview_screen.dart delete mode 100644 packages/flutter_chat_view/lib/src/screens/new_group_chat_screen.dart delete mode 100644 packages/flutter_chat_view/lib/src/services/profile_service.dart delete mode 100644 packages/flutter_chat_view/pubspec.yaml delete mode 100644 packages/flutter_chat_view/test/flutter_community_chat_test.dart diff --git a/.gitignore b/.gitignore index 3a6d73f..26a33ff 100644 --- a/.gitignore +++ b/.gitignore @@ -39,9 +39,14 @@ build/ pubspec.lock packages/flutter_chat/pubspec.lock -packages/flutter_chat_firebase/pubspec.lock -packages/flutter_chat_interface/pubspec.lock -packages/flutter_chat_view/pubspec.lock +packages/firebase_chat_repository/pubspec.lock +packages/chat_repository_interface/pubspec.lock + +android +linux +macos +web +windows pubspec_overrides.yaml diff --git a/packages/chat_repository_interface/.gitignore b/packages/chat_repository_interface/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/packages/chat_repository_interface/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# 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/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/packages/chat_repository_interface/CHANGELOG.md b/packages/chat_repository_interface/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/packages/chat_repository_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/chat_repository_interface/LICENSE b/packages/chat_repository_interface/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/packages/chat_repository_interface/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/chat_repository_interface/README.md b/packages/chat_repository_interface/README.md new file mode 100644 index 0000000..02fe8ec --- /dev/null +++ b/packages/chat_repository_interface/README.md @@ -0,0 +1,39 @@ + + +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 new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/packages/chat_repository_interface/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/chat_repository_interface/lib/chat_repository_interface.dart b/packages/chat_repository_interface/lib/chat_repository_interface.dart new file mode 100644 index 0000000..5fa1623 --- /dev/null +++ b/packages/chat_repository_interface/lib/chat_repository_interface.dart @@ -0,0 +1,17 @@ +library chat_repository_interface; + +// Interfaces +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'; + +// Models +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'; 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 new file mode 100644 index 0000000..ebb4202 --- /dev/null +++ b/packages/chat_repository_interface/lib/src/interfaces/chat_repostory_interface.dart @@ -0,0 +1,55 @@ +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'; + +abstract class ChatRepositoryInterface { + String createChat({ + required List users, + String? chatName, + String? description, + String? imageUrl, + List? messages, + }); + + Stream updateChat({ + required ChatModel chat, + }); + + Stream getChat({ + required String chatId, + }); + + Stream?> getChats({ + required String userId, + }); + + Stream?> getMessages({ + required String chatId, + required String userId, + required int pageSize, + required int page, + }); + + bool sendMessage({ + required String chatId, + required String senderId, + String? text, + String? imageUrl, + }); + + bool deleteChat({ + required String chatId, + }); + + Stream getUnreadMessagesCount({ + required String userId, + String? chatId, + }); + + 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 new file mode 100644 index 0000000..b18912f --- /dev/null +++ b/packages/chat_repository_interface/lib/src/interfaces/user_repository_interface.dart @@ -0,0 +1,7 @@ +import 'package:chat_repository_interface/src/models/user_model.dart'; + +abstract class UserRepositoryInterface { + Stream getUser({required String userId}); + + 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 new file mode 100644 index 0000000..a8faac7 --- /dev/null +++ b/packages/chat_repository_interface/lib/src/local/local_chat_repository.dart @@ -0,0 +1,214 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:chat_repository_interface/chat_repository_interface.dart'; +import 'package:collection/collection.dart'; +import 'package:rxdart/rxdart.dart'; + +class LocalChatRepository implements ChatRepositoryInterface { + LocalChatRepository() { + var messages = []; + + 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 = + BehaviorSubject>(); + + StreamController chatController = BehaviorSubject(); + + StreamController> messageController = + BehaviorSubject>(); + + List _chats = []; + + @override + String createChat( + {required List users, + String? chatName, + String? description, + String? imageUrl, + List? messages}) { + var chat = ChatModel( + id: DateTime.now().toString(), + users: users, + messages: messages ?? [], + chatName: chatName, + description: description, + imageUrl: imageUrl, + ); + + _chats.add(chat); + chatsController.add(_chats); + + return chat.id; + } + + @override + Stream updateChat({required ChatModel chat}) { + var index = _chats.indexWhere((e) => e.id == chat.id); + + if (index != -1) { + _chats[index] = chat; + chatsController.add(_chats); + } + + return chatController.stream.where((e) => e.id == chat.id); + } + + @override + bool deleteChat({required String chatId}) { + try { + _chats.removeWhere((e) => e.id == chatId); + chatsController.add(_chats); + + return true; + } catch (e) { + return false; + } + } + + @override + Stream getChat({required String chatId}) { + var chat = _chats.firstWhereOrNull((e) => e.id == chatId); + + if (chat != null) { + chatController.add(chat); + + if (chat.imageUrl != null && chat.imageUrl!.isNotEmpty) { + chat.copyWith(imageUrl: 'https://picsum.photos/200/300'); + } + } + + return chatController.stream; + } + + @override + Stream?> getChats({required String userId}) { + chatsController.add(_chats); + + return chatsController.stream; + } + + @override + Stream?> getMessages({ + required String chatId, + required String userId, + required int pageSize, + required int page, + }) { + ChatModel? chat; + + chat = _chats.firstWhereOrNull((e) => e.id == chatId); + + if (chat != null) { + var messages = List.from(chat.messages); + + 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(); + + if (newMessages.isEmpty) return; + + var allMessages = [...oldMessages, ...newMessages]; + + allMessages = allMessages + .toSet() + .toList() + .cast() + .toList(growable: false); + + 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()); + }); + } + + return messageController.stream; + } + + @override + bool sendMessage( + {required String chatId, + required String senderId, + String? text, + String? imageUrl}) { + var message = MessageModel( + id: DateTime.now().toString(), + timestamp: DateTime.now(), + text: text, + senderId: senderId, + imageUrl: imageUrl, + ); + + var chat = _chats.firstWhereOrNull((e) => e.id == chatId); + + if (chat == null) return false; + + chat.messages.add(message); + messageController.add(chat.messages); + + return true; + } + + @override + Stream getUnreadMessagesCount({required String userId, String? chatId}) { + return chatsController.stream.map((chats) { + var count = 0; + + for (var chat in chats) { + if (chat.users.any((e) => e.id == userId)) { + count += chat.unreadMessageCount; + } + } + + return count; + }); + } + + @override + Future uploadImage({ + required String path, + required Uint8List image, + }) { + return 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 new file mode 100644 index 0000000..0965bb8 --- /dev/null +++ b/packages/chat_repository_interface/lib/src/local/local_user_repository.dart @@ -0,0 +1,52 @@ +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'; + +class LocalUserRepository implements UserRepositoryInterface { + final StreamController> _usersController = + BehaviorSubject>(); + + final List _users = [ + UserModel( + 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', + ), + UserModel( + 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', + ), + ]; + + @override + Stream getUser({required String userId}) { + return getAllUsers().map((users) => users.firstWhere( + (e) => e.id == userId, + orElse: () => throw Exception(), + )); + } + + @override + Stream> getAllUsers() { + _usersController.add(_users); + + return _usersController.stream; + } +} diff --git a/packages/chat_repository_interface/lib/src/models/chat_model.dart b/packages/chat_repository_interface/lib/src/models/chat_model.dart new file mode 100644 index 0000000..d4d5655 --- /dev/null +++ b/packages/chat_repository_interface/lib/src/models/chat_model.dart @@ -0,0 +1,65 @@ +import 'package:chat_repository_interface/src/models/message_model.dart'; +import 'package:chat_repository_interface/src/models/user_model.dart'; + +class ChatModel { + ChatModel({ + required this.id, + required this.users, + required this.messages, + this.chatName, + this.description, + this.imageUrl, + this.canBeDeleted = true, + this.lastUsed, + this.lastMessage, + this.unreadMessageCount = 0, + }); + + final String id; + final List messages; + final List users; + final String? chatName; + final String? description; + final String? imageUrl; + + final bool canBeDeleted; + final DateTime? lastUsed; + final MessageModel? lastMessage; + final int unreadMessageCount; + + ChatModel copyWith({ + String? id, + List? messages, + List? users, + String? chatName, + String? description, + String? imageUrl, + bool? canBeDeleted, + DateTime? lastUsed, + MessageModel? 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; +} + +extension GetOtherUser on ChatModel { + UserModel getOtherUser(String userId) { + return users.firstWhere((user) => user.id != 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 new file mode 100644 index 0000000..c839ace --- /dev/null +++ b/packages/chat_repository_interface/lib/src/models/message_model.dart @@ -0,0 +1,37 @@ +class MessageModel { + MessageModel({ + required this.id, + required this.text, + required this.imageUrl, + required this.timestamp, + required this.senderId, + }); + + final String id; + final String? text; + final String? imageUrl; + final DateTime timestamp; + final String senderId; + + MessageModel copyWith({ + 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, + ); + } +} + +extension MessageType on MessageModel { + bool isTextMessage() => text != null; + + bool 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 new file mode 100644 index 0000000..9d2d576 --- /dev/null +++ b/packages/chat_repository_interface/lib/src/models/user_model.dart @@ -0,0 +1,31 @@ +class UserModel { + UserModel({ + required this.id, + this.firstName, + this.lastName, + this.imageUrl, + }); + + final String id; + final String? firstName; + final String? lastName; + final String? imageUrl; +} + +extension Fullname on UserModel { + String? get fullname { + if (firstName == null && lastName == null) { + return null; + } + + if (firstName == null) { + return lastName; + } + + if (lastName == null) { + return firstName; + } + + return "$firstName $lastName"; + } +} diff --git a/packages/chat_repository_interface/lib/src/services/chat_service.dart b/packages/chat_repository_interface/lib/src/services/chat_service.dart new file mode 100644 index 0000000..e95e181 --- /dev/null +++ b/packages/chat_repository_interface/lib/src/services/chat_service.dart @@ -0,0 +1,176 @@ +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'; + +class ChatService { + final ChatRepositoryInterface chatRepository; + final UserRepositoryInterface userRepository; + + ChatService({ + ChatRepositoryInterface? chatRepository, + UserRepositoryInterface? userRepository, + }) : chatRepository = chatRepository ?? LocalChatRepository(), + userRepository = userRepository ?? LocalUserRepository(); + + Stream createChat({ + required List users, + String? chatName, + String? description, + String? imageUrl, + List? messages, + }) { + var chatId = chatRepository.createChat( + users: users, + chatName: chatName, + description: description, + imageUrl: imageUrl, + messages: messages, + ); + + return chatRepository.getChat(chatId: chatId); + } + + Stream?> getChats({ + required String userId, + }) { + return chatRepository.getChats(userId: userId); + } + + Stream getChat({ + required String chatId, + }) { + return chatRepository.getChat(chatId: chatId); + } + + Future getChatByUser({ + required String currentUser, + required String otherUser, + }) async { + var chats = await chatRepository + .getChats(userId: currentUser) + .first + .timeout(const Duration(seconds: 1)); + + var personalChats = + chats?.where((element) => element.users.length == 2).toList(); + + return personalChats?.firstWhereOrNull( + (element) => element.users.where((e) => e.id == otherUser).isNotEmpty, + ); + } + + 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 groupChats = personalChats + ?.where((chats) => otherUsers.every(chats.users.contains)) + .toList(); + + return groupChats?.firstWhereOrNull( + (element) => + element.chatName == chatName && element.description == description, + ); + } catch (e) { + return null; + } + } + + 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, + ); + } + + bool sendMessage({ + required String chatId, + String? text, + required String senderId, + String? imageUrl, + }) { + return chatRepository.sendMessage( + chatId: chatId, + text: text, + senderId: senderId, + imageUrl: imageUrl, + ); + } + + bool deleteChat({ + required String chatId, + }) { + return chatRepository.deleteChat(chatId: chatId); + } + + Stream getUser({required String userId}) { + return userRepository.getUser(userId: userId); + } + + Stream> getAllUsers() { + return userRepository.getAllUsers(); + } + + Stream getUnreadMessagesCount({ + required String userId, + String? chatId, + }) { + if (chatId == null) { + return chatRepository.getUnreadMessagesCount(userId: userId); + } + + return chatRepository.getUnreadMessagesCount( + userId: userId, + chatId: chatId, + ); + } + + Future uploadImage({ + required String path, + required Uint8List image, + }) { + return chatRepository.uploadImage( + path: path, + image: image, + ); + } + + Future markAsRead({ + required String chatId, + }) async { + var chat = await chatRepository.getChat(chatId: chatId).first; + + var newChat = chat.copyWith( + lastUsed: DateTime.now(), + unreadMessageCount: 0, + ); + + chatRepository.updateChat(chat: newChat); + } +} diff --git a/packages/chat_repository_interface/pubspec.yaml b/packages/chat_repository_interface/pubspec.yaml new file mode 100644 index 0000000..b9d789e --- /dev/null +++ b/packages/chat_repository_interface/pubspec.yaml @@ -0,0 +1,57 @@ +name: chat_repository_interface +description: "A new Flutter package project." +version: 0.0.1 +homepage: + +environment: + sdk: '>=3.4.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + rxdart: any + collection: any + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.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 diff --git a/packages/firebase_chat_repository/.gitignore b/packages/firebase_chat_repository/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/packages/firebase_chat_repository/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# 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/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/packages/firebase_chat_repository/CHANGELOG.md b/packages/firebase_chat_repository/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/packages/firebase_chat_repository/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/firebase_chat_repository/LICENSE b/packages/firebase_chat_repository/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/packages/firebase_chat_repository/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/firebase_chat_repository/README.md b/packages/firebase_chat_repository/README.md new file mode 100644 index 0000000..02fe8ec --- /dev/null +++ b/packages/firebase_chat_repository/README.md @@ -0,0 +1,39 @@ + + +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/firebase_chat_repository/analysis_options.yaml b/packages/firebase_chat_repository/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/packages/firebase_chat_repository/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/firebase_chat_repository/lib/firebase_chat_repository.dart b/packages/firebase_chat_repository/lib/firebase_chat_repository.dart new file mode 100644 index 0000000..4ca7268 --- /dev/null +++ b/packages/firebase_chat_repository/lib/firebase_chat_repository.dart @@ -0,0 +1,7 @@ +library firebase_chat_repository; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/packages/firebase_chat_repository/pubspec.yaml b/packages/firebase_chat_repository/pubspec.yaml new file mode 100644 index 0000000..d67d044 --- /dev/null +++ b/packages/firebase_chat_repository/pubspec.yaml @@ -0,0 +1,54 @@ +name: firebase_chat_repository +description: "A new Flutter package project." +version: 0.0.1 +homepage: + +environment: + sdk: '>=3.4.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.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 diff --git a/packages/flutter_chat/.gitignore b/packages/flutter_chat/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/packages/flutter_chat/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# 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/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/packages/flutter_chat/CHANGELOG.md~72104f3 (feat: refactor) b/packages/flutter_chat/CHANGELOG.md~72104f3 (feat: refactor) new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/packages/flutter_chat/CHANGELOG.md~72104f3 (feat: refactor) @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/flutter_chat/LICENSE~72104f3 (feat: refactor) b/packages/flutter_chat/LICENSE~72104f3 (feat: refactor) new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/packages/flutter_chat/LICENSE~72104f3 (feat: refactor) @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/flutter_chat/README.md~72104f3 (feat: refactor) b/packages/flutter_chat/README.md~72104f3 (feat: refactor) new file mode 100644 index 0000000..02fe8ec --- /dev/null +++ b/packages/flutter_chat/README.md~72104f3 (feat: refactor) @@ -0,0 +1,39 @@ + + +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/flutter_chat/analysis_options.yaml b/packages/flutter_chat/analysis_options.yaml index 31b4b51..a5744c1 100644 --- a/packages/flutter_chat/analysis_options.yaml +++ b/packages/flutter_chat/analysis_options.yaml @@ -1,9 +1,4 @@ -include: package:flutter_iconica_analysis/analysis_options.yaml +include: package:flutter_lints/flutter.yaml -# Possible to overwrite the rules from the package - -analyzer: - exclude: - -linter: - rules: +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/flutter_chat/example/.gitignore b/packages/flutter_chat/example/.gitignore index dcf4fd3..29a3a50 100644 --- a/packages/flutter_chat/example/.gitignore +++ b/packages/flutter_chat/example/.gitignore @@ -15,7 +15,6 @@ migrate_working_dir/ *.ipr *.iws .idea/ -ios # 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 @@ -32,14 +31,6 @@ ios .pub/ /build/ -# Platform-specific folders -**/android/ -**/ios/ -**/web/ -**/windows/ -**/macos/ -**/linux/ - # Symbolication related app.*.symbols diff --git a/packages/flutter_chat/example/analysis_options.yaml b/packages/flutter_chat/example/analysis_options.yaml index 31b4b51..0d29021 100644 --- a/packages/flutter_chat/example/analysis_options.yaml +++ b/packages/flutter_chat/example/analysis_options.yaml @@ -1,9 +1,28 @@ -include: package:flutter_iconica_analysis/analysis_options.yaml +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. -# Possible to overwrite the rules from the package - -analyzer: - exclude: +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/flutter_chat/example/lib/main.dart b/packages/flutter_chat/example/lib/main.dart index 5aa4ee8..8bfb96e 100644 --- a/packages/flutter_chat/example/lib/main.dart +++ b/packages/flutter_chat/example/lib/main.dart @@ -20,13 +20,7 @@ class Home extends StatelessWidget { const Home({super.key}); @override - Widget build(BuildContext context) => Center( - child: chatNavigatorUserStory( - context, - configuration: ChatUserStoryConfiguration( - chatService: LocalChatService(), - chatOptionsBuilder: (ctx) => const ChatOptions(), - ), - ), + Widget build(BuildContext context) => const Center( + child: FlutterChatEntryWidget(userId: '1'), ); } diff --git a/packages/flutter_chat/example/pubspec.yaml b/packages/flutter_chat/example/pubspec.yaml index 09249cc..ffe8be3 100644 --- a/packages/flutter_chat/example/pubspec.yaml +++ b/packages/flutter_chat/example/pubspec.yaml @@ -1,29 +1,92 @@ name: example description: "A new Flutter project." -publish_to: "none" +# 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. version: 1.0.0+1 environment: - sdk: ">=3.2.5 <4.0.0" + sdk: '>=3.4.3 <4.0.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 - cupertino_icons: ^1.0.2 - firebase_core: ^2.24.2 - firebase_auth: ^4.16.0 + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.6 flutter_chat: path: ../ - flutter_chat_firebase: - path: ../../flutter_chat_firebase dev_dependencies: flutter_test: sdk: flutter - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 + # 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 + +# 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 deleted file mode 100644 index 1f7b336..0000000 --- a/packages/flutter_chat/example/test/widget_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -// 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_test/flutter_test.dart"; - -void main() { - testWidgets("Counter increments smoke test", (WidgetTester tester) async { - expect(true, true); - }); -} diff --git a/packages/flutter_chat/lib/flutter_chat.dart b/packages/flutter_chat/lib/flutter_chat.dart index d3f4e4e..b208ad3 100644 --- a/packages/flutter_chat/lib/flutter_chat.dart +++ b/packages/flutter_chat/lib/flutter_chat.dart @@ -1,14 +1,11 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause -/// library flutter_chat; -export "package:flutter_chat/src/chat_entry_widget.dart"; +// 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"; -export "package:flutter_chat/src/flutter_chat_userstory.dart"; -export "package:flutter_chat/src/models/chat_configuration.dart"; -export "package:flutter_chat/src/routes.dart"; -export "package:flutter_chat_interface/flutter_chat_interface.dart"; -export "package:flutter_chat_local/local_chat_service.dart"; -export "package:flutter_chat_view/flutter_chat_view.dart"; diff --git a/packages/flutter_chat/lib/src/config/chat_builders.dart b/packages/flutter_chat/lib/src/config/chat_builders.dart new file mode 100644 index 0000000..646d35d --- /dev/null +++ b/packages/flutter_chat/lib/src/config/chat_builders.dart @@ -0,0 +1,98 @@ +import 'package:chat_repository_interface/chat_repository_interface.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_chat/src/config/chat_translations.dart'; + +class ChatBuilders { + const ChatBuilders({ + this.chatScreenScaffoldBuilder, + this.newChatScreenScaffoldBuilder, + this.newGroupChatScreenScaffoldBuilder, + this.newGroupChatOverviewScaffoldBuilder, + this.chatProfileScaffoldBuilder, + this.messageInputBuilder, + this.chatDetailScaffoldBuilder, + this.chatRowContainerBuilder, + this.groupAvatarBuilder, + this.imagePickerContainerBuilder, + this.userAvatarBuilder, + this.deleteChatDialogBuilder, + this.newChatButtonBuilder, + this.noUsersPlaceholderBuilder, + this.chatTitleBuilder, + this.usernameBuilder, + this.loadingWidgetBuilder, + }); + + final ScaffoldBuilder? chatScreenScaffoldBuilder; + final ScaffoldBuilder? newChatScreenScaffoldBuilder; + final ScaffoldBuilder? newGroupChatOverviewScaffoldBuilder; + final ScaffoldBuilder? newGroupChatScreenScaffoldBuilder; + final ScaffoldBuilder? chatDetailScaffoldBuilder; + final ScaffoldBuilder? chatProfileScaffoldBuilder; + + final TextInputBuilder? messageInputBuilder; + + final ContainerBuilder? chatRowContainerBuilder; + + final GroupAvatarBuilder? groupAvatarBuilder; + + final UserAvatarBuilder? userAvatarBuilder; + + final Future Function(BuildContext, ChatModel)? + deleteChatDialogBuilder; + + final ButtonBuilder? newChatButtonBuilder; + + final NoUsersPlaceholderBuilder? noUsersPlaceholderBuilder; + + final Widget Function(String chatTitle)? chatTitleBuilder; + + final Widget Function(String userFullName)? usernameBuilder; + + final ImagePickerContainerBuilder? imagePickerContainerBuilder; + + final Widget? Function(BuildContext context)? loadingWidgetBuilder; +} + +typedef ButtonBuilder = Widget Function( + BuildContext context, + VoidCallback onPressed, + ChatTranslations translations, +); + +typedef ImagePickerContainerBuilder = Widget Function( + VoidCallback onClose, + ChatTranslations translations, + BuildContext context, +); + +typedef TextInputBuilder = Widget Function( + TextEditingController textEditingController, + Widget suffixIcon, + ChatTranslations translations, +); + +typedef ScaffoldBuilder = Scaffold Function( + AppBar appBar, + Widget body, + Color backgroundColor, +); + +typedef ContainerBuilder = Widget Function( + Widget child, +); + +typedef GroupAvatarBuilder = Widget Function( + String groupName, + String? imageUrl, + double size, +); + +typedef UserAvatarBuilder = Widget Function( + UserModel user, + double size, +); + +typedef NoUsersPlaceholderBuilder = Widget Function( + ChatTranslations translations, +); diff --git a/packages/flutter_chat/lib/src/config/chat_options.dart b/packages/flutter_chat/lib/src/config/chat_options.dart new file mode 100644 index 0000000..8015ddc --- /dev/null +++ b/packages/flutter_chat/lib/src/config/chat_options.dart @@ -0,0 +1,28 @@ +import 'dart:ui'; + +import 'package:flutter_chat/src/config/chat_builders.dart'; +import 'package:flutter_chat/src/config/chat_translations.dart'; + +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({ + this.dateformat, + this.groupChatEnabled = true, + this.showTimes = true, + this.translations = const ChatTranslations.empty(), + this.builders = const ChatBuilders(), + this.iconEnabledColor = const Color(0xFF212121), + this.iconDisabledColor = const Color(0xFF9E9E9E), + this.onNoChats, + this.pageSize = 20, + }); +} diff --git a/packages/flutter_chat_view/lib/src/config/chat_translations.dart b/packages/flutter_chat/lib/src/config/chat_translations.dart similarity index 96% rename from packages/flutter_chat_view/lib/src/config/chat_translations.dart rename to packages/flutter_chat/lib/src/config/chat_translations.dart index 6c3ee2d..45a5c52 100644 --- a/packages/flutter_chat_view/lib/src/config/chat_translations.dart +++ b/packages/flutter_chat/lib/src/config/chat_translations.dart @@ -29,7 +29,6 @@ class ChatTranslations { required this.deleteChatModalConfirm, required this.noUsersFound, required this.noChatsFound, - required this.chatCantBeDeleted, required this.chatProfileUsers, required this.imagePickerTitle, required this.uploadFile, @@ -46,6 +45,8 @@ class ChatTranslations { required this.groupBioFieldHeader, required this.selectedMembersHeader, required this.createGroupChatButton, + required this.groupNameEmpty, + required this.next, }); /// Default translations for the chat component view @@ -72,7 +73,6 @@ class ChatTranslations { this.noUsersFound = "No users were found to start a chat with", this.noChatsFound = "Click on 'Start a chat' to create a new chat", this.anonymousUser = "Anonymous user", - this.chatCantBeDeleted = "This chat can't be deleted", this.chatProfileUsers = "Members:", this.imagePickerTitle = "Do you want to upload a file or take a picture?", this.uploadFile = "UPLOAD FILE", @@ -89,6 +89,8 @@ class ChatTranslations { this.groupBioFieldHeader = "Additional information for members", this.selectedMembersHeader = "Members: ", this.createGroupChatButton = "Create groupchat", + this.groupNameEmpty = "Group", + this.next = "Next", }); final String chatsTitle; @@ -110,7 +112,6 @@ class ChatTranslations { final String deleteChatModalConfirm; final String noUsersFound; final String noChatsFound; - final String chatCantBeDeleted; final String chatProfileUsers; final String imagePickerTitle; final String uploadFile; @@ -129,6 +130,9 @@ class ChatTranslations { final String groupBioHintText; final String groupProfileBioHeader; final String groupBioValidatorEmpty; + final String groupNameEmpty; + + final String next; // copyWith method to override the default values ChatTranslations copyWith({ @@ -151,7 +155,6 @@ class ChatTranslations { String? deleteChatModalConfirm, String? noUsersFound, String? noChatsFound, - String? chatCantBeDeleted, String? chatProfileUsers, String? imagePickerTitle, String? uploadFile, @@ -168,6 +171,8 @@ class ChatTranslations { String? groupBioFieldHeader, String? selectedMembersHeader, String? createGroupChatButton, + String? groupNameEmpty, + String? next, }) => ChatTranslations( chatsTitle: chatsTitle ?? this.chatsTitle, @@ -194,7 +199,6 @@ class ChatTranslations { deleteChatModalConfirm ?? this.deleteChatModalConfirm, noUsersFound: noUsersFound ?? this.noUsersFound, noChatsFound: noChatsFound ?? this.noChatsFound, - chatCantBeDeleted: chatCantBeDeleted ?? this.chatCantBeDeleted, chatProfileUsers: chatProfileUsers ?? this.chatProfileUsers, imagePickerTitle: imagePickerTitle ?? this.imagePickerTitle, uploadFile: uploadFile ?? this.uploadFile, @@ -218,5 +222,7 @@ class ChatTranslations { selectedMembersHeader ?? this.selectedMembersHeader, createGroupChatButton: createGroupChatButton ?? this.createGroupChatButton, + groupNameEmpty: groupNameEmpty ?? this.groupNameEmpty, + next: next ?? this.next, ); } diff --git a/packages/flutter_chat/lib/src/chat_entry_widget.dart b/packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart similarity index 85% rename from packages/flutter_chat/lib/src/chat_entry_widget.dart rename to packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart index a0e6dc8..449c147 100644 --- a/packages/flutter_chat/lib/src/chat_entry_widget.dart +++ b/packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart @@ -4,9 +4,10 @@ import "package:flutter/material.dart"; import "package:flutter_chat/flutter_chat.dart"; /// A widget representing an entry point for a chat UI. -class ChatEntryWidget extends StatefulWidget { - /// Constructs a [ChatEntryWidget]. - const ChatEntryWidget({ +class FlutterChatEntryWidget extends StatefulWidget { + /// Constructs a [FlutterChatEntryWidget]. + const FlutterChatEntryWidget({ + required this.userId, this.chatService, this.onTap, this.widgetSize = 75, @@ -21,6 +22,9 @@ class ChatEntryWidget extends StatefulWidget { /// The chat service associated with the widget. final ChatService? chatService; + /// The user ID of the person currently looking at the chat + final String userId; + /// Background color of the widget. final Color backgroundColor; @@ -43,17 +47,17 @@ class ChatEntryWidget extends StatefulWidget { final TextStyle? textStyle; @override - State createState() => _ChatEntryWidgetState(); + State createState() => _FlutterChatEntryWidgetState(); } -/// State class for [ChatEntryWidget]. -class _ChatEntryWidgetState extends State { +/// State class for [FlutterChatEntryWidget]. +class _FlutterChatEntryWidgetState extends State { ChatService? chatService; @override void initState() { super.initState(); - chatService ??= widget.chatService ?? LocalChatService(); + chatService ??= widget.chatService ?? ChatService(); } @override @@ -62,17 +66,14 @@ class _ChatEntryWidgetState extends State { widget.onTap?.call() ?? Navigator.of(context).pushReplacement( MaterialPageRoute( - builder: (context) => chatNavigatorUserStory( - context, - configuration: ChatUserStoryConfiguration( - chatService: chatService!, - chatOptionsBuilder: (ctx) => const ChatOptions(), - ), + builder: (context) => FlutterChatNavigatorUserstory( + userId: widget.userId, + chatService: chatService!, ), ), ), child: StreamBuilder( - stream: chatService!.chatOverviewService.getUnreadChatsCountStream(), + stream: chatService!.getUnreadMessagesCount(userId: widget.userId), builder: (BuildContext context, snapshot) => Stack( alignment: Alignment.center, children: [ @@ -154,8 +155,8 @@ class _AnimatedNotificationIconState extends State<_AnimatedNotificationIcon> @override void dispose() { - super.dispose(); _animationController.dispose(); + super.dispose(); } @override 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 d7d76ff..7fbe759 100644 --- a/packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart +++ b/packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart @@ -4,354 +4,219 @@ import "package:flutter/material.dart"; import "package:flutter_chat/flutter_chat.dart"; +import "package:flutter_chat/src/screens/chat_detail_screen.dart"; +import "package:flutter_chat/src/screens/chat_profile_screen.dart"; +import "package:flutter_chat/src/screens/chat_screen.dart"; +import "package:flutter_chat/src/screens/creation/new_chat_screen.dart"; +import "package:flutter_chat/src/screens/creation/new_group_chat_overview.dart"; +import "package:flutter_chat/src/screens/creation/new_group_chat_screen.dart"; -/// Navigates to the chat user story screen. -/// -/// [context]: The build context. -/// [configuration]: The configuration for the chat user story. -Widget chatNavigatorUserStory( - BuildContext context, { - ChatUserStoryConfiguration? configuration, -}) => - _chatScreenRoute( - configuration ?? - ChatUserStoryConfiguration( - chatService: LocalChatService(), - chatOptionsBuilder: (ctx) => const ChatOptions(), - ), - context, +class FlutterChatNavigatorUserstory extends StatefulWidget { + const FlutterChatNavigatorUserstory({ + super.key, + required this.userId, + this.chatService, + this.chatOptions, + }); + + final String userId; + + final ChatService? chatService; + final ChatOptions? chatOptions; + + @override + State createState() => + _FlutterChatNavigatorUserstoryState(); +} + +class _FlutterChatNavigatorUserstoryState + extends State { + late ChatService chatService; + late ChatOptions chatOptions; + + @override + void initState() { + chatService = widget.chatService ?? ChatService(); + chatOptions = widget.chatOptions ?? ChatOptions(); + super.initState(); + } + + @override + Widget build(BuildContext context) => chatScreen(); + + 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()); + }, ); + } -/// Constructs the chat screen route widget. -/// -/// [configuration]: The configuration for the chat user story. -/// [context]: The build context. -Widget _chatScreenRoute( - ChatUserStoryConfiguration configuration, - BuildContext context, -) => - PopScope( - canPop: configuration.onPopInvoked == null, - onPopInvoked: (didPop) => - configuration.onPopInvoked?.call(didPop, context), - child: ChatScreen( - unreadMessageTextStyle: configuration.unreadMessageTextStyle, - service: configuration.chatService, - options: configuration.chatOptionsBuilder(context), - onNoChats: () async => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _newChatScreenRoute( - configuration, - context, - ), - ), + Widget chatDetailScreen(ChatModel chat) => ChatDetailScreen( + userId: widget.userId, + chatService: chatService, + chatOptions: chatOptions, + chat: chat, + onReadChat: (chat) => chatService.markAsRead( + chatId: chat.id, ), - onPressStartChat: () async { - if (configuration.onPressStartChat != null) { - return await configuration.onPressStartChat?.call(); + onPressChatTitle: (chat) { + if (chat.isGroupChat) { + return route(chatProfileScreen(null, chat)); } - return Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _newChatScreenRoute( - configuration, - context, - ), - ), + var otherUser = chat.getOtherUser(widget.userId); + + return route(chatProfileScreen(otherUser, null)); + }, + onPressUserProfile: (user) { + return route(chatProfileScreen(user, null)); + }, + onUploadImage: (data) async { + var path = await chatService.uploadImage(path: 'chats', image: data); + + chatService.sendMessage( + chatId: chat.id, + senderId: widget.userId, + imageUrl: path, ); }, - onPressChat: (chat) async => - configuration.onPressChat?.call(context, chat) ?? - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _chatDetailScreenRoute( - configuration, - context, - chat.id!, - ), - ), - ), - onDeleteChat: (chat) async => - configuration.onDeleteChat?.call(context, chat) ?? - configuration.chatService.chatOverviewService.deleteChat(chat), - deleteChatDialog: configuration.deleteChatDialog, - translations: configuration.translations, - ), - ); + onMessageSubmit: (text) { + chatService.sendMessage( + chatId: chat.id, + senderId: widget.userId, + text: text, + ); + }, + ); -/// Constructs the chat detail screen route widget. -/// -/// [configuration]: The configuration for the chat user story. -/// [context]: The build context. -/// [chatId]: The id of the chat. -Widget _chatDetailScreenRoute( - ChatUserStoryConfiguration configuration, - BuildContext context, - String chatId, -) => - ChatDetailScreen( - chatTitleBuilder: configuration.chatTitleBuilder, - usernameBuilder: configuration.usernameBuilder, - loadingWidgetBuilder: configuration.loadingWidgetBuilder, - iconDisabledColor: configuration.iconDisabledColor, - pageSize: configuration.messagePageSize, - options: configuration.chatOptionsBuilder(context), - translations: configuration.translations, - service: configuration.chatService, - chatId: chatId, - textfieldBottomPadding: configuration.textfieldBottomPadding ?? 0, - onPressUserProfile: (user) async { - if (configuration.onPressUserProfile != null) { - return configuration.onPressUserProfile?.call(context, user); - } - var currentUser = - await configuration.chatService.chatUserService.getCurrentUser(); - var currentUserId = currentUser!.id!; - if (context.mounted) - return Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _chatProfileScreenRoute( - configuration, - context, - chatId, - user.id, - currentUserId, - ), - ), - ); - }, - onMessageSubmit: (message) async { - if (configuration.onMessageSubmit != null) { - await configuration.onMessageSubmit?.call(message); - } else { - await configuration.chatService.chatDetailService - .sendTextMessage(chatId: chatId, text: message); - } + Widget chatProfileScreen(UserModel? user, ChatModel? chat) => + ChatProfileScreen( + options: chatOptions, + userId: widget.userId, + userModel: user, + chatModel: chat, + onTapUser: (user) { + route(chatProfileScreen(user, null)); + }, + onPressStartChat: (user) async { + var chat = await createChat(user.id); + return route(chatDetailScreen(chat)); + }, + ); - configuration.afterMessageSent?.call(chatId); - }, - onUploadImage: (image) async { - if (configuration.onUploadImage != null) { - await configuration.onUploadImage?.call(image); - } else { - await configuration.chatService.chatDetailService - .sendImageMessage(chatId: chatId, image: image); - } + Widget newChatScreen() => NewChatScreen( + userId: widget.userId, + chatService: chatService, + chatOptions: chatOptions, + onPressCreateGroupChat: () { + return route(newGroupChatScreen()); + }, + onPressCreateChat: (user) async { + var chat = await createChat(user.id); + return route(chatDetailScreen(chat)); + }, + ); - configuration.afterMessageSent?.call(chatId); - }, - onReadChat: (chat) async => - configuration.onReadChat?.call(chat) ?? - configuration.chatService.chatOverviewService.readChat(chat), - onPressChatTitle: (context, chat) async { - if (configuration.onPressChatTitle?.call(context, chat) != null) { - return configuration.onPressChatTitle?.call(context, chat); - } - var currentUser = - await configuration.chatService.chatUserService.getCurrentUser(); - var currentUserId = currentUser!.id!; - if (context.mounted) - return Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _chatProfileScreenRoute( - configuration, - context, - chatId, - null, - currentUserId, - ), - ), - ); - }, - iconColor: configuration.iconColor, - ); + Widget newGroupChatScreen() => NewGroupChatScreen( + userId: widget.userId, + chatService: chatService, + chatOptions: chatOptions, + onContinue: (users) { + return route(newGroupChatOverview(users)); + }, + ); -/// Constructs the chat profile screen route widget. -/// -/// [configuration]: The configuration for the chat user story. -/// [context]: The build context. -/// [chatId]: The id of the chat. -/// [userId]: The id of the user. -Widget _chatProfileScreenRoute( - ChatUserStoryConfiguration configuration, - BuildContext context, - String chatId, - String? userId, - String currentUserId, -) => - ChatProfileScreen( - options: configuration.chatOptionsBuilder(context), - translations: configuration.translations, - chatService: configuration.chatService, - chatId: chatId, - userId: userId, - currentUserId: currentUserId, - onTapUser: (user) async { - if (configuration.onPressUserProfile != null) { - return configuration.onPressUserProfile!.call(context, user); - } - var currentUser = - await configuration.chatService.chatUserService.getCurrentUser(); - var currentUserId = currentUser!.id!; - if (context.mounted) - return Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _chatProfileScreenRoute( - configuration, - context, - chatId, - user.id, - currentUserId, - ), - ), - ); - }, - onPressStartChat: (user) async { - configuration.onPressCreateChat?.call(user); - if (configuration.onPressCreateChat != null) return; - var chat = await configuration.chatService.chatOverviewService - .getChatByUser(user); - if (chat.id == null) { - chat = await configuration.chatService.chatOverviewService - .storeChatIfNot( - PersonalChatModel( - user: user, - ), - null, - ); - } - if (context.mounted) { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => PopScope( - canPop: false, - child: _chatDetailScreenRoute( - configuration, - context, - chat.id!, - ), - ), - ), - ); - } - }, - ); - -/// Constructs the new chat screen route widget. -/// -/// [configuration]: The configuration for the chat user story. -/// [context]: The build context. -Widget _newChatScreenRoute( - ChatUserStoryConfiguration configuration, - BuildContext context, -) => - NewChatScreen( - options: configuration.chatOptionsBuilder(context), - translations: configuration.translations, - service: configuration.chatService, - showGroupChatButton: configuration.enableGroupChatCreation, - onPressCreateGroupChat: () async { - configuration.onPressCreateGroupChat?.call(); - configuration.chatService.chatOverviewService - .clearCurrentlySelectedUsers(); - if (context.mounted) { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _newGroupChatScreenRoute( - configuration, - context, - ), - ), - ); - } - }, - onPressCreateChat: (user) async { - configuration.onPressCreateChat?.call(user); - if (configuration.onPressCreateChat != null) return; - var chat = await configuration.chatService.chatOverviewService - .getChatByUser(user); - if (chat.id == null) { - chat = await configuration.chatService.chatOverviewService - .storeChatIfNot( - PersonalChatModel( - user: user, - ), - null, - ); - } - if (context.mounted) { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => PopScope( - canPop: false, - child: _chatDetailScreenRoute( - configuration, - context, - chat.id!, - ), - ), - ), - ); - } - }, - ); - -Widget _newGroupChatScreenRoute( - ChatUserStoryConfiguration configuration, - BuildContext context, -) => - NewGroupChatScreen( - options: configuration.chatOptionsBuilder(context), - translations: configuration.translations, - service: configuration.chatService, - onPressGroupChatOverview: (users) async => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _newGroupChatOverviewScreenRoute( - configuration, - context, + Widget newGroupChatOverview(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); + } + var chat = await createGroupChat( users, - ), - ), - ), - ); - -Widget _newGroupChatOverviewScreenRoute( - ChatUserStoryConfiguration configuration, - BuildContext context, - List users, -) => - NewGroupChatOverviewScreen( - options: configuration.chatOptionsBuilder(context), - translations: configuration.translations, - service: configuration.chatService, - onPressCompleteGroupChatCreation: - (users, groupChatName, groupBio, image) async { - configuration.onPressCompleteGroupChatCreation - ?.call(users, groupChatName, image); - if (configuration.onPressCreateGroupChat != null) return; - var chat = - await configuration.chatService.chatOverviewService.storeChatIfNot( - GroupChatModel( - canBeDeleted: true, - title: groupChatName, - users: users, - bio: groupBio, - ), - image, - ); - if (context.mounted) { - await Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => PopScope( - canPop: false, - child: _chatDetailScreenRoute( - configuration, - context, - chat.id!, - ), - ), - ), + title, + description, + path, ); - } - }, + return route(chatDetailScreen(chat)); + }, + ); + + Future createGroupChat( + List userModels, + String title, + String description, + String? imageUrl, + ) async { + ChatModel? chat; + + try { + chat = await chatService.getGroupChatByUser( + currentUser: widget.userId, + otherUsers: userModels, + chatName: title, + description: description, + ); + } catch (e) { + chat = null; + } + + if (chat == null) { + var currentUser = await chatService.getUser(userId: widget.userId).first; + var otherUsers = await Future.wait( + userModels.map((e) => chatService.getUser(userId: e.id).first), + ); + + chat = await chatService.createChat( + users: [currentUser, ...otherUsers], + chatName: title, + description: description, + imageUrl: imageUrl, + ).first; + } + + return chat; + } + + Future createChat(String otherUserId) async { + ChatModel? chat; + + try { + chat = await chatService.getChatByUser( + currentUser: widget.userId, + otherUser: otherUserId, + ); + } catch (e) { + chat = null; + } + + if (chat == null) { + var currentUser = await chatService.getUser(userId: widget.userId).first; + var otherUser = await chatService.getUser(userId: otherUserId).first; + + chat = await chatService.createChat( + users: [currentUser, otherUser], + ).first; + } + + return chat; + } + + void route(Widget screen) { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => screen), ); + } +} diff --git a/packages/flutter_chat/lib/src/flutter_chat_userstory.dart b/packages/flutter_chat/lib/src/flutter_chat_userstory.dart deleted file mode 100644 index 0aedc8f..0000000 --- a/packages/flutter_chat/lib/src/flutter_chat_userstory.dart +++ /dev/null @@ -1,325 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter/material.dart"; -import "package:flutter_chat/flutter_chat.dart"; -import "package:flutter_chat/src/go_router.dart"; -import "package:go_router/go_router.dart"; - -List getChatStoryRoutes( - ChatUserStoryConfiguration configuration, -) => - [ - GoRoute( - path: ChatUserStoryRoutes.chatScreen, - pageBuilder: (context, state) { - var service = configuration.chatServiceBuilder?.call(context) ?? - configuration.chatService; - var chatScreen = ChatScreen( - unreadMessageTextStyle: configuration.unreadMessageTextStyle, - service: service, - options: configuration.chatOptionsBuilder(context), - onNoChats: () async => - context.push(ChatUserStoryRoutes.newChatScreen), - onPressStartChat: () async { - if (configuration.onPressStartChat != null) { - return await configuration.onPressStartChat?.call(); - } - - return context.push(ChatUserStoryRoutes.newChatScreen); - }, - onPressChat: (chat) async => - configuration.onPressChat?.call(context, chat) ?? - context.push(ChatUserStoryRoutes.chatDetailViewPath(chat.id!)), - onDeleteChat: (chat) async => - configuration.onDeleteChat?.call(context, chat) ?? - configuration.chatService.chatOverviewService.deleteChat(chat), - deleteChatDialog: configuration.deleteChatDialog, - translations: configuration.translationsBuilder?.call(context) ?? - configuration.translations, - ); - return buildScreenWithoutTransition( - context: context, - state: state, - child: PopScope( - canPop: configuration.onPopInvoked == null, - onPopInvoked: (didPop) => - configuration.onPopInvoked?.call(didPop, context), - child: configuration.chatPageBuilder?.call( - context, - chatScreen, - ) ?? - Scaffold( - body: chatScreen, - ), - ), - ); - }, - ), - GoRoute( - path: ChatUserStoryRoutes.chatDetailScreen, - pageBuilder: (context, state) { - var chatId = state.pathParameters["id"]; - var service = configuration.chatServiceBuilder?.call(context) ?? - configuration.chatService; - - var chatDetailScreen = ChatDetailScreen( - chatTitleBuilder: configuration.chatTitleBuilder, - usernameBuilder: configuration.usernameBuilder, - loadingWidgetBuilder: configuration.loadingWidgetBuilder, - iconDisabledColor: configuration.iconDisabledColor, - pageSize: configuration.messagePageSize, - options: configuration.chatOptionsBuilder(context), - translations: configuration.translationsBuilder?.call(context) ?? - configuration.translations, - service: service, - chatId: chatId!, - textfieldBottomPadding: configuration.textfieldBottomPadding ?? 0, - onPressUserProfile: (user) async { - if (configuration.onPressUserProfile != null) { - return configuration.onPressUserProfile?.call(context, user); - } - return context.push( - ChatUserStoryRoutes.chatProfileScreenPath(chatId, user.id), - ); - }, - onMessageSubmit: (message) async { - if (configuration.onMessageSubmit != null) { - await configuration.onMessageSubmit?.call(message); - } else { - await configuration.chatService.chatDetailService - .sendTextMessage(chatId: chatId, text: message); - } - configuration.afterMessageSent?.call(chatId); - }, - onUploadImage: (image) async { - if (configuration.onUploadImage?.call(image) != null) { - await configuration.onUploadImage?.call(image); - } else { - await configuration.chatService.chatDetailService - .sendImageMessage(chatId: chatId, image: image); - } - configuration.afterMessageSent?.call(chatId); - }, - onReadChat: (chat) async => - configuration.onReadChat?.call(chat) ?? - configuration.chatService.chatOverviewService.readChat(chat), - onPressChatTitle: (context, chat) async { - if (configuration.onPressChatTitle?.call(context, chat) != null) { - return configuration.onPressChatTitle?.call(context, chat); - } - - return context.push( - ChatUserStoryRoutes.chatProfileScreenPath(chat.id!, null), - ); - }, - iconColor: configuration.iconColor, - ); - return buildScreenWithoutTransition( - context: context, - state: state, - child: configuration.chatPageBuilder?.call( - context, - chatDetailScreen, - ) ?? - Scaffold( - body: chatDetailScreen, - ), - ); - }, - ), - GoRoute( - path: ChatUserStoryRoutes.newChatScreen, - pageBuilder: (context, state) { - var service = configuration.chatServiceBuilder?.call(context) ?? - configuration.chatService; - - var newChatScreen = NewChatScreen( - options: configuration.chatOptionsBuilder(context), - translations: configuration.translationsBuilder?.call(context) ?? - configuration.translations, - service: service, - showGroupChatButton: configuration.enableGroupChatCreation, - onPressCreateChat: (user) async { - configuration.onPressCreateChat?.call(user); - if (configuration.onPressCreateChat != null) return; - var chat = await configuration.chatService.chatOverviewService - .getChatByUser(user); - if (chat.id == null) { - chat = await configuration.chatService.chatOverviewService - .storeChatIfNot( - PersonalChatModel( - user: user, - ), - null, - ); - } - if (context.mounted) { - await context.push( - ChatUserStoryRoutes.chatDetailViewPath(chat.id ?? ""), - ); - } - }, - onPressCreateGroupChat: () async { - configuration.chatService.chatOverviewService - .clearCurrentlySelectedUsers(); - return context.push( - ChatUserStoryRoutes.newGroupChatScreen, - ); - }, - ); - return buildScreenWithoutTransition( - context: context, - state: state, - child: configuration.chatPageBuilder?.call( - context, - newChatScreen, - ) ?? - Scaffold( - body: newChatScreen, - ), - ); - }, - ), - GoRoute( - path: ChatUserStoryRoutes.newGroupChatScreen, - pageBuilder: (context, state) { - var service = configuration.chatServiceBuilder?.call(context) ?? - configuration.chatService; - - var newGroupChatScreen = NewGroupChatScreen( - options: configuration.chatOptionsBuilder(context), - translations: configuration.translationsBuilder?.call(context) ?? - configuration.translations, - service: service, - onPressGroupChatOverview: (users) async => context.push( - ChatUserStoryRoutes.newGroupChatOverviewScreen, - extra: users, - ), - ); - return buildScreenWithoutTransition( - context: context, - state: state, - child: configuration.chatPageBuilder?.call( - context, - newGroupChatScreen, - ) ?? - Scaffold( - body: newGroupChatScreen, - ), - ); - }, - ), - GoRoute( - path: ChatUserStoryRoutes.newGroupChatOverviewScreen, - pageBuilder: (context, state) { - var service = configuration.chatServiceBuilder?.call(context) ?? - configuration.chatService; - - var newGroupChatOverviewScreen = NewGroupChatOverviewScreen( - options: configuration.chatOptionsBuilder(context), - translations: configuration.translationsBuilder?.call(context) ?? - configuration.translations, - service: service, - onPressCompleteGroupChatCreation: - (users, groupChatName, groupBio, image) async { - configuration.onPressCompleteGroupChatCreation - ?.call(users, groupChatName, image); - var chat = await configuration.chatService.chatOverviewService - .storeChatIfNot( - GroupChatModel( - canBeDeleted: true, - title: groupChatName, - users: users, - bio: groupBio, - ), - image, - ); - if (context.mounted) { - context.go( - ChatUserStoryRoutes.chatDetailViewPath(chat.id ?? ""), - ); - } - }, - ); - return buildScreenWithoutTransition( - context: context, - state: state, - child: configuration.chatPageBuilder?.call( - context, - newGroupChatOverviewScreen, - ) ?? - Scaffold( - body: newGroupChatOverviewScreen, - ), - ); - }, - ), - GoRoute( - path: ChatUserStoryRoutes.chatProfileScreen, - pageBuilder: (context, state) { - var chatId = state.pathParameters["id"]; - var userId = state.pathParameters["userId"]; - var id = userId == "null" ? null : userId; - var service = configuration.chatServiceBuilder?.call(context) ?? - configuration.chatService; - ChatUserModel? currentUser; - String? currentUserId; - Future.delayed(Duration.zero, () async { - currentUser = await service.chatUserService.getCurrentUser(); - currentUserId = currentUser!.id; - }); - - var profileScreen = ChatProfileScreen( - options: configuration.chatOptionsBuilder(context), - translations: configuration.translationsBuilder?.call(context) ?? - configuration.translations, - chatService: service, - chatId: chatId!, - userId: id, - currentUserId: currentUserId!, - onTapUser: (user) async { - if (configuration.onPressUserProfile != null) { - return configuration.onPressUserProfile!.call(context, user); - } - - return context.push( - ChatUserStoryRoutes.chatProfileScreenPath(chatId, user.id), - ); - }, - onPressStartChat: (user) async { - configuration.onPressCreateChat?.call(user); - if (configuration.onPressCreateChat != null) return; - var chat = await configuration.chatService.chatOverviewService - .getChatByUser(user); - if (chat.id == null) { - chat = await configuration.chatService.chatOverviewService - .storeChatIfNot( - PersonalChatModel( - user: user, - ), - null, - ); - } - if (context.mounted) { - await context.push( - ChatUserStoryRoutes.chatDetailViewPath(chat.id ?? ""), - ); - } - }, - ); - return buildScreenWithoutTransition( - context: context, - state: state, - child: configuration.chatPageBuilder?.call( - context, - profileScreen, - ) ?? - Scaffold( - body: profileScreen, - ), - ); - }, - ), - ]; diff --git a/packages/flutter_chat/lib/src/go_router.dart b/packages/flutter_chat/lib/src/go_router.dart deleted file mode 100644 index e3c15c2..0000000 --- a/packages/flutter_chat/lib/src/go_router.dart +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter/material.dart"; -import "package:go_router/go_router.dart"; - -/// Builds a screen with a fade transition. -/// -/// [context]: The build context. -/// [state]: The state of the GoRouter. -/// [child]: The child widget to be displayed. -CustomTransitionPage buildScreenWithFadeTransition({ - required BuildContext context, - required GoRouterState state, - required Widget child, -}) => - CustomTransitionPage( - key: state.pageKey, - child: child, - transitionsBuilder: (context, animation, secondaryAnimation, child) => - FadeTransition(opacity: animation, child: child), - ); - -/// Builds a screen without any transition. -/// -/// [context]: The build context. -/// [state]: The state of the GoRouter. -/// [child]: The child widget to be displayed. -CustomTransitionPage buildScreenWithoutTransition({ - required BuildContext context, - required GoRouterState state, - required Widget child, -}) => - CustomTransitionPage( - key: state.pageKey, - child: child, - transitionsBuilder: (context, animation, secondaryAnimation, child) => - child, - ); diff --git a/packages/flutter_chat/lib/src/models/chat_configuration.dart b/packages/flutter_chat/lib/src/models/chat_configuration.dart deleted file mode 100644 index 06b9c6f..0000000 --- a/packages/flutter_chat/lib/src/models/chat_configuration.dart +++ /dev/null @@ -1,143 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "dart:typed_data"; - -import "package:flutter/material.dart"; -import "package:flutter_chat_view/flutter_chat_view.dart"; - -/// `ChatUserStoryConfiguration` is a class that configures the chat user story. -@immutable -class ChatUserStoryConfiguration { - /// Creates a new instance of `ChatUserStoryConfiguration`. - const ChatUserStoryConfiguration({ - required this.chatService, - required this.chatOptionsBuilder, - this.chatServiceBuilder, - this.onPressStartChat, - this.onPressChat, - this.onDeleteChat, - this.onMessageSubmit, - this.onReadChat, - this.onUploadImage, - this.onPopInvoked, - this.onPressCreateChat, - this.onPressCreateGroupChat, - this.onPressCompleteGroupChatCreation, - this.iconColor = Colors.black, - this.deleteChatDialog, - this.disableDismissForPermanentChats = false, - this.routeToNewChatIfEmpty = true, - this.enableGroupChatCreation = true, - this.translations = const ChatTranslations.empty(), - this.translationsBuilder, - this.chatPageBuilder, - this.onPressChatTitle, - this.afterMessageSent, - this.messagePageSize = 20, - this.onPressUserProfile, - this.textfieldBottomPadding = 20, - this.iconDisabledColor = Colors.grey, - this.unreadMessageTextStyle, - this.loadingWidgetBuilder, - this.usernameBuilder, - this.chatTitleBuilder, - }); - - /// The service responsible for handling chat-related functionalities. - final ChatService chatService; - - /// A method to get the chat service only when needed and with a context. - final ChatService Function(BuildContext context)? chatServiceBuilder; - - /// Callback function triggered when a chat is pressed. - final Function(BuildContext, ChatModel)? onPressChat; - - /// Callback function triggered when a chat is deleted. - final Function(BuildContext, ChatModel)? onDeleteChat; - - /// Translations for internationalization/localization support. - final ChatTranslations translations; - - /// Translations builder because context might be needed for translations. - final ChatTranslations Function(BuildContext context)? translationsBuilder; - - /// Determines whether dismissing is disabled for permanent chats. - final bool disableDismissForPermanentChats; - - /// Callback function for uploading an image. - final Future Function(Uint8List image)? onUploadImage; - - /// Callback function for submitting a message. - final Future Function(String text)? onMessageSubmit; - - /// Called after a new message is sent. This can be used to do something - /// extra like sending a push notification. - final Function(String chatId)? afterMessageSent; - - /// Callback function triggered when a chat is read. - final Future Function(ChatModel chat)? onReadChat; - - /// Callback function triggered when creating a chat. - final Function(ChatUserModel)? onPressCreateChat; - - /// Builder for chat options based on context. - final Function( - List users, - String groupchatName, - Uint8List? image, - )? onPressCompleteGroupChatCreation; - - final Function()? onPressCreateGroupChat; - - /// Builder for the chat options which can be used to style the UI of the chat - final ChatOptions Function(BuildContext context) chatOptionsBuilder; - - /// If true, the user will be routed to the new chat screen if there are - /// no chats. - final bool routeToNewChatIfEmpty; - - /// The size of each page of messages. - final int messagePageSize; - - /// Whether to enable group chat creation for the user. If false, - /// the button will be hidden - final bool enableGroupChatCreation; - - /// Dialog for confirming chat deletion. - final Future Function(BuildContext, ChatModel)? deleteChatDialog; - - /// Callback function triggered when chat title is pressed. - final Function(BuildContext context, ChatModel chat)? onPressChatTitle; - - /// Color of icons. - final Color? iconColor; - - /// Builder for the chat page. - final Widget Function(BuildContext context, Widget child)? chatPageBuilder; - - /// Callback function triggered when starting a chat. - final Function()? onPressStartChat; - - /// Callback function triggered when user profile is pressed. - final Function(BuildContext context, ChatUserModel user)? onPressUserProfile; - - /// Callback function triggered when the popscope on the chat - /// homepage is triggered. - // ignore: avoid_positional_boolean_parameters - final Function(bool didPop, BuildContext context)? onPopInvoked; - - final double? textfieldBottomPadding; - - final Color? iconDisabledColor; - - /// The text style used for the unread message counter. - final TextStyle? unreadMessageTextStyle; - - final Widget? Function(BuildContext context)? loadingWidgetBuilder; - - final Widget Function(String userFullName)? usernameBuilder; - - final Widget Function(String chatTitle)? chatTitleBuilder; -} diff --git a/packages/flutter_chat/lib/src/routes.dart b/packages/flutter_chat/lib/src/routes.dart deleted file mode 100644 index b9e4581..0000000 --- a/packages/flutter_chat/lib/src/routes.dart +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -/// Provides route paths for the chat user story. -mixin ChatUserStoryRoutes { - static const String chatScreen = "/chat"; - - /// Constructs the path for the chat detail view. - static String chatDetailViewPath(String chatId) => "/chat-detail/$chatId"; - - static const String chatDetailScreen = "/chat-detail/:id"; - static const String newChatScreen = "/new-chat"; - - /// Constructs the path for the chat profile screen. - static const String newGroupChatScreen = "/new-group-chat"; - static const String newGroupChatOverviewScreen = "/new-group-chat-overview"; - static String chatProfileScreenPath(String chatId, String? userId) => - "/chat-profile/$chatId/$userId"; - - static const String chatProfileScreen = "/chat-profile/:id/:userId"; -} diff --git a/packages/flutter_chat/lib/src/screens/chat_detail_screen.dart b/packages/flutter_chat/lib/src/screens/chat_detail_screen.dart new file mode 100644 index 0000000..bd1f55d --- /dev/null +++ b/packages/flutter_chat/lib/src/screens/chat_detail_screen.dart @@ -0,0 +1,661 @@ +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'; + +class ChatDetailScreen extends StatefulWidget { + const ChatDetailScreen({ + super.key, + required this.userId, + required this.chatService, + required this.chatOptions, + required this.chat, + required this.onPressChatTitle, + required this.onPressUserProfile, + required this.onUploadImage, + required this.onMessageSubmit, + required this.onReadChat, + }); + + final String userId; + final ChatService chatService; + final ChatOptions chatOptions; + final ChatModel chat; + final Function(ChatModel) onPressChatTitle; + final Function(UserModel) onPressUserProfile; + final Function(Uint8List image) onUploadImage; + final Function(String text) onMessageSubmit; + final Function(ChatModel chat) onReadChat; + + @override + State createState() => _ChatDetailScreenState(); +} + +class _ChatDetailScreenState extends State { + late String chatTitle; + + @override + void initState() { + if (widget.chat.isGroupChat) { + chatTitle = widget.chat.chatName ?? + widget.chatOptions.translations.groupNameEmpty; + } else { + chatTitle = widget.chat.users + .firstWhere((element) => element.id != widget.userId) + .fullname ?? + widget.chatOptions.translations.anonymousUser; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return widget.chatOptions.builders.chatDetailScaffoldBuilder?.call( + _AppBar( + chatTitle: chatTitle, + chatOptions: widget.chatOptions, + onPressChatTitle: widget.onPressChatTitle, + chatModel: widget.chat, + ) as AppBar, + _Body( + chatService: widget.chatService, + options: widget.chatOptions, + chat: widget.chat, + currentUserId: widget.userId, + onPressUserProfile: widget.onPressUserProfile, + onUploadImage: widget.onUploadImage, + onMessageSubmit: widget.onMessageSubmit, + onReadChat: widget.onReadChat, + ), + theme.scaffoldBackgroundColor, + ) ?? + Scaffold( + appBar: _AppBar( + chatTitle: chatTitle, + chatOptions: widget.chatOptions, + onPressChatTitle: widget.onPressChatTitle, + chatModel: widget.chat, + ), + body: _Body( + chatService: widget.chatService, + options: widget.chatOptions, + chat: widget.chat, + currentUserId: widget.userId, + onPressUserProfile: widget.onPressUserProfile, + onUploadImage: widget.onUploadImage, + onMessageSubmit: widget.onMessageSubmit, + onReadChat: widget.onReadChat, + ), + ); + } +} + +class _AppBar extends StatelessWidget implements PreferredSizeWidget { + const _AppBar({ + required this.chatTitle, + required this.chatOptions, + required this.onPressChatTitle, + required this.chatModel, + }); + + final String chatTitle; + final ChatOptions chatOptions; + final Function(ChatModel) onPressChatTitle; + final ChatModel chatModel; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return AppBar( + iconTheme: theme.appBarTheme.iconTheme ?? + const IconThemeData(color: Colors.white), + centerTitle: true, + leading: GestureDetector( + onTap: () { + Navigator.popUntil(context, (route) => route.isFirst); + }, + child: const Icon( + Icons.arrow_back_ios, + ), + ), + title: GestureDetector( + onTap: () => onPressChatTitle.call(chatModel), + child: chatOptions.builders.chatTitleBuilder?.call(chatTitle) ?? + Text( + chatTitle, + overflow: TextOverflow.ellipsis, + ), + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +class _Body extends StatefulWidget { + const _Body({ + required this.chatService, + required this.options, + required this.chat, + required this.currentUserId, + required this.onPressUserProfile, + required this.onUploadImage, + required this.onMessageSubmit, + required this.onReadChat, + }); + + final ChatService chatService; + final ChatOptions options; + final String currentUserId; + final ChatModel chat; + final Function(UserModel) onPressUserProfile; + final Function(Uint8List image) onUploadImage; + final Function(String message) onMessageSubmit; + final Function(ChatModel chat) onReadChat; + + @override + State<_Body> createState() => _BodyState(); +} + +class _BodyState extends State<_Body> { + ScrollController controller = ScrollController(); + bool showIndicator = false; + late int pageSize; + var page = 0; + + @override + void initState() { + pageSize = widget.options.pageSize; + super.initState(); + } + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + 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(), + ); + } + + var messages = snapshot.data?.reversed.toList() ?? []; + + 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, + ), + ), + ], + for (var i = 0; i < messages.length; i++) ...[ + _ChatBubble( + key: ValueKey(messages[i].id), + message: messages[i], + previousMessage: i < messages.length - 1 + ? messages[i + 1] + : null, + chatService: widget.chatService, + onPressUserProfile: widget.onPressUserProfile, + options: widget.options, + ), + ] + ], + ), + ); + }), + ), + _ChatBottom( + chat: widget.chat, + onPressSelectImage: () async => onPressSelectImage.call( + context, + widget.options, + widget.onUploadImage, + ), + onMessageSubmit: widget.onMessageSubmit, + options: widget.options, + ), + ], + ), + if (showIndicator) ...[ + widget.options.builders.loadingWidgetBuilder?.call(context) ?? + const Column( + children: [ + SizedBox( + height: 10, + ), + Center( + child: CircularProgressIndicator(), + ), + SizedBox( + height: 10, + ), + ], + ), + ], + ], + ); + } +} + +class _ChatBottom extends StatefulWidget { + const _ChatBottom({ + required this.chat, + required this.onMessageSubmit, + required this.options, + this.onPressSelectImage, + }); + + /// Callback function invoked when a message is submitted. + final Function(String text) onMessageSubmit; + + /// Callback function invoked when the select image button is pressed. + final VoidCallback? onPressSelectImage; + + /// The chat model. + final ChatModel chat; + + final ChatOptions options; + + @override + State<_ChatBottom> createState() => _ChatBottomState(); +} + +class _ChatBottomState extends State<_ChatBottom> { + final TextEditingController _textEditingController = TextEditingController(); + bool _isTyping = false; + bool _isSending = false; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + _textEditingController.addListener(() { + if (_textEditingController.text.isEmpty) { + setState(() { + _isTyping = false; + }); + } else { + setState(() { + _isTyping = true; + }); + } + }); + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 16, + ), + child: SizedBox( + height: 45, + child: widget.options.builders.messageInputBuilder?.call( + _textEditingController, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: widget.onPressSelectImage, + icon: Icon( + Icons.image_outlined, + color: widget.options.iconEnabledColor, + ), + ), + IconButton( + disabledColor: widget.options.iconDisabledColor, + color: widget.options.iconEnabledColor, + onPressed: _isTyping && !_isSending + ? () async { + setState(() { + _isSending = true; + }); + + var value = _textEditingController.text; + + if (value.isNotEmpty) { + await widget.onMessageSubmit(value); + _textEditingController.clear(); + } + + setState(() { + _isSending = false; + }); + } + : null, + icon: const Icon( + Icons.send, + ), + ), + ], + ), + widget.options.translations, + ) ?? + TextField( + style: theme.textTheme.bodySmall, + textCapitalization: TextCapitalization.sentences, + controller: _textEditingController, + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: const BorderSide( + color: Colors.black, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: const BorderSide( + color: Colors.black, + ), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 0, + horizontal: 30, + ), + hintText: widget.options.translations.messagePlaceholder, + hintStyle: theme.textTheme.bodyMedium!.copyWith( + color: theme.textTheme.bodyMedium!.color!.withOpacity(0.5), + ), + fillColor: Colors.white, + filled: true, + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(25), + ), + borderSide: BorderSide.none, + ), + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: widget.onPressSelectImage, + icon: Icon( + Icons.image_outlined, + color: widget.options.iconEnabledColor, + ), + ), + IconButton( + disabledColor: widget.options.iconDisabledColor, + color: widget.options.iconEnabledColor, + onPressed: _isTyping && !_isSending + ? () async { + setState(() { + _isSending = true; + }); + + var value = _textEditingController.text; + + if (value.isNotEmpty) { + await widget.onMessageSubmit(value); + _textEditingController.clear(); + } + + setState(() { + _isSending = false; + }); + } + : null, + icon: const Icon( + Icons.send, + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _ChatBubble extends StatefulWidget { + const _ChatBubble({ + required this.message, + required this.chatService, + required this.onPressUserProfile, + required this.options, + this.previousMessage, + super.key, + }); + final ChatOptions options; + final ChatService chatService; + final MessageModel message; + final MessageModel? previousMessage; + final Function(UserModel user) onPressUserProfile; + + @override + State<_ChatBubble> createState() => _ChatBubbleState(); +} + +class _ChatBubbleState extends State<_ChatBubble> { + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var translations = widget.options.translations; + var dateFormatter = DateFormatter(options: widget.options); + + var isNewDate = widget.previousMessage != null && + widget.message.timestamp.day != widget.previousMessage?.timestamp.day; + var isSameSender = widget.previousMessage == null || + widget.previousMessage?.senderId != widget.message.senderId; + var isSameMinute = widget.previousMessage != null && + widget.message.timestamp.minute == + 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(), + ); + } + + 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( + 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(), + ), + ], + ), + ), + ), + ], + ), + ); + }); + } +} + +class _ChatImage extends StatelessWidget { + const _ChatImage({ + required this.image, + }); + + final String image; + + @override + Widget build(BuildContext context) => Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(40.0), + ), + width: 40, + height: 40, + child: image.isNotEmpty + ? CachedNetworkImage( + fadeInDuration: Duration.zero, + imageUrl: image, + fit: BoxFit.cover, + ) + : null, + ); +} diff --git a/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart b/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart new file mode 100644 index 0000000..8e451ef --- /dev/null +++ b/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart @@ -0,0 +1,275 @@ +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'; + +class ChatProfileScreen extends StatelessWidget { + const ChatProfileScreen({ + super.key, + required this.options, + required this.userId, + required this.userModel, + required this.chatModel, + required this.onTapUser, + required this.onPressStartChat, + }); + + final ChatOptions options; + final String userId; + final UserModel? userModel; + final ChatModel? chatModel; + final Function(UserModel)? onTapUser; + final Function(UserModel)? onPressStartChat; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return options.builders.chatProfileScaffoldBuilder?.call( + _AppBar( + user: userModel, + chat: chatModel, + options: options, + ) as AppBar, + _Body( + currentUser: userId, + options: options, + user: userModel, + chat: chatModel, + onTapUser: onTapUser, + onPressStartChat: onPressStartChat, + ), + theme.scaffoldBackgroundColor, + ) ?? + Scaffold( + appBar: _AppBar( + user: userModel, + chat: chatModel, + options: options, + ), + body: _Body( + currentUser: userId, + options: options, + user: userModel, + chat: chatModel, + onTapUser: onTapUser, + onPressStartChat: onPressStartChat, + ), + ); + } +} + +class _AppBar extends StatelessWidget implements PreferredSizeWidget { + const _AppBar({ + required this.user, + required this.chat, + required this.options, + }); + + final UserModel? user; + final ChatModel? chat; + final ChatOptions options; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return AppBar( + iconTheme: theme.appBarTheme.iconTheme ?? + const IconThemeData(color: Colors.white), + title: Text( + user != null + ? '${user!.fullname}' + : chat != null + ? chat?.chatName ?? options.translations.groupNameEmpty + : "", + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +class _Body extends StatelessWidget { + const _Body({ + required this.options, + required this.user, + required this.chat, + required this.onPressStartChat, + required this.onTapUser, + required this.currentUser, + }); + + final ChatOptions options; + final UserModel? user; + final ChatModel? chat; + final Function(UserModel)? onTapUser; + final Function(UserModel)? onPressStartChat; + final String currentUser; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Stack( + children: [ + ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + children: [ + options.builders.userAvatarBuilder?.call( + user ?? + ( + chat != null + ? UserModel( + id: UniqueKey().toString(), + firstName: chat?.chatName, + imageUrl: chat?.imageUrl, + ) + : UserModel( + id: UniqueKey().toString(), + firstName: + options.translations.groupNameEmpty, + ), + ) as UserModel, + 60, + ) ?? + Avatar( + boxfit: BoxFit.cover, + user: user != null + ? User( + firstName: user?.firstName, + lastName: user?.lastName, + imageUrl: user?.imageUrl != null || + user?.imageUrl != "" + ? user?.imageUrl + : null, + ) + : chat != null + ? User( + firstName: chat?.chatName, + imageUrl: chat?.imageUrl != null || + chat?.imageUrl != "" + ? chat?.imageUrl + : null, + ) + : User( + firstName: + options.translations.groupNameEmpty, + ), + size: 60, + ), + ], + ), + ), + const Divider( + color: Colors.white, + thickness: 10, + ), + if (chat != null) ...[ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 20, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + options.translations.groupProfileBioHeader, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 12, + ), + Text( + chat!.description ?? "", + style: theme.textTheme.bodyMedium! + .copyWith(color: Colors.black), + ), + const SizedBox( + height: 12, + ), + Text( + options.translations.chatProfileUsers, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 12, + ), + Wrap( + children: [ + ...chat!.users.map( + (tappedUser) => Padding( + padding: const EdgeInsets.only( + bottom: 8, + right: 8, + ), + child: GestureDetector( + onTap: () { + onTapUser?.call(tappedUser); + }, + child: Column( + 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 + : null, + ), + size: 60, + ), + ], + ), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ], + ), + if (user != null && user!.id != currentUser) ...[ + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 80, + ), + child: FilledButton( + onPressed: () { + onPressStartChat?.call(user!); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + options.translations.newChatButton, + style: theme.textTheme.displayLarge, + ), + ], + ), + ), + ), + ), + ], + ], + ); + } +} diff --git a/packages/flutter_chat/lib/src/screens/chat_screen.dart b/packages/flutter_chat/lib/src/screens/chat_screen.dart new file mode 100644 index 0000000..a330089 --- /dev/null +++ b/packages/flutter_chat/lib/src/screens/chat_screen.dart @@ -0,0 +1,588 @@ +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"; + +class ChatScreen extends StatelessWidget { + const ChatScreen({ + super.key, + required this.userId, + required this.chatService, + required this.chatOptions, + required this.onPressChat, + required this.onDeleteChat, + this.onPressStartChat, + }); + + final String userId; + final ChatService chatService; + final ChatOptions chatOptions; + + /// Callback function for starting a chat. + final Function()? onPressStartChat; + + /// Callback function for pressing on a chat. + final void Function(ChatModel chat) onPressChat; + + final void Function(ChatModel chat) onDeleteChat; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return chatOptions.builders.chatScreenScaffoldBuilder?.call( + _AppBar( + userId: userId, + chatOptions: chatOptions, + chatService: chatService, + ) as AppBar, + _Body( + userId: userId, + chatOptions: chatOptions, + chatService: chatService, + onPressChat: onPressChat, + onPressStartChat: onPressStartChat, + onDeleteChat: onDeleteChat, + ), + theme.scaffoldBackgroundColor, + ) ?? + Scaffold( + appBar: _AppBar( + userId: userId, + chatOptions: chatOptions, + chatService: chatService, + ), + body: _Body( + userId: userId, + chatOptions: chatOptions, + chatService: chatService, + onPressChat: onPressChat, + onPressStartChat: onPressStartChat, + onDeleteChat: onDeleteChat, + ), + ); + } +} + +class _AppBar extends StatelessWidget implements PreferredSizeWidget { + const _AppBar({ + required this.userId, + required this.chatOptions, + required this.chatService, + }); + + final String userId; + final ChatOptions chatOptions; + final ChatService chatService; + + @override + Widget build(BuildContext context) { + var translations = chatOptions.translations; + var theme = Theme.of(context); + + return AppBar( + title: Text( + translations.chatsTitle, + ), + actions: [ + StreamBuilder( + stream: chatService.getUnreadMessagesCount(userId: userId), + builder: (BuildContext context, snapshot) => Align( + alignment: Alignment.centerRight, + child: Visibility( + visible: (snapshot.data ?? 0) > 0, + child: Padding( + padding: const EdgeInsets.only(right: 22.0), + child: Text( + "${snapshot.data ?? 0} ${translations.chatsUnread}", + style: theme.textTheme.bodySmall, + ), + ), + ), + ), + ), + ], + ); + } + + @override + Size get preferredSize => const Size.fromHeight( + kToolbarHeight, + ); +} + +class _Body extends StatefulWidget { + const _Body({ + required this.userId, + required this.chatOptions, + required this.chatService, + required this.onPressChat, + required this.onDeleteChat, + this.onPressStartChat, + }); + + final String userId; + final ChatOptions chatOptions; + final ChatService chatService; + final Function(ChatModel chat) onPressChat; + final Function()? onPressStartChat; + final Function(ChatModel) onDeleteChat; + + @override + State<_Body> createState() => _BodyState(); +} + +class _BodyState extends State<_Body> { + ScrollController controller = ScrollController(); + bool _hasCalledOnNoChats = false; + + @override + Widget build(BuildContext context) { + var translations = widget.chatOptions.translations; + var theme = Theme.of(context); + return Column( + children: [ + Expanded( + child: ListView( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 28), + children: [ + 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)) { + if (widget.chatOptions.onNoChats != null && + !_hasCalledOnNoChats) { + _hasCalledOnNoChats = true; // Set the flag to true + WidgetsBinding.instance.addPostFrameCallback((_) async { + await widget.chatOptions.onNoChats!.call(); + }); + } + return Center( + child: Text( + translations.noChatsFound, + style: theme.textTheme.bodySmall, + ), + ); + } + return Column( + children: [ + for (ChatModel chat in (snapshot.data ?? [])) ...[ + DecoratedBox( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: theme.dividerColor, + width: 0.5, + ), + ), + ), + child: Builder( + builder: (context) => !chat.canBeDeleted + ? Dismissible( + confirmDismiss: (_) async { + widget.chatOptions.builders + .deleteChatDialogBuilder + ?.call(context, chat) ?? + _deleteDialog( + chat, + translations, + context, + ); + return _deleteDialog( + chat, + translations, + context, + ); + }, + onDismissed: (_) { + widget.onDeleteChat(chat); + }, + secondaryBackground: const ColoredBox( + color: Colors.red, + child: Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.delete, + color: Colors.white, + ), + ), + ), + ), + background: const ColoredBox( + color: Colors.red, + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.delete, + color: Colors.white, + ), + ), + ), + ), + key: ValueKey( + chat.id.toString(), + ), + child: ChatListItem( + chat: chat, + chatOptions: widget.chatOptions, + userId: widget.userId, + onPressChat: widget.onPressChat, + ), + ) + : ChatListItem( + chat: chat, + chatOptions: widget.chatOptions, + userId: widget.userId, + onPressChat: widget.onPressChat, + ), + ), + ), + ], + ], + ); + }, + ), + ], + ), + ), + if (widget.onPressStartChat != null) + widget.chatOptions.builders.newChatButtonBuilder?.call( + context, + widget.onPressStartChat!, + translations, + ) ?? + Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 4, + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + fixedSize: const Size(254, 44), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(56), + ), + ), + onPressed: widget.onPressStartChat!, + child: Text( + translations.newChatButton, + style: theme.textTheme.displayLarge, + ), + ), + ), + ], + ); + } +} + +class ChatListItem extends StatelessWidget { + const ChatListItem({ + required this.chat, + required this.chatOptions, + required this.userId, + required this.onPressChat, + super.key, + }); + + final ChatModel chat; + final ChatOptions chatOptions; + final String userId; + final Function(ChatModel chat) onPressChat; + + @override + Widget build(BuildContext context) { + var dateFormatter = DateFormatter( + options: chatOptions, + ); + var theme = Theme.of(context); + return GestureDetector( + onTap: () { + onPressChat(chat); + }, + child: chatOptions.builders.chatRowContainerBuilder?.call( + _ChatListItem( + chat: chat, + options: chatOptions, + dateFormatter: dateFormatter, + currentUserId: userId, + ), + ) ?? + DecoratedBox( + decoration: BoxDecoration( + color: Colors.transparent, + border: Border( + bottom: BorderSide( + color: theme.dividerColor, + width: 0.5, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: _ChatListItem( + chat: chat, + options: chatOptions, + dateFormatter: dateFormatter, + currentUserId: userId, + ), + ), + ), + ); + } +} + +class _ChatListItem extends StatelessWidget { + const _ChatListItem({ + required this.chat, + required this.options, + required this.dateFormatter, + required this.currentUserId, + }); + + final ChatModel chat; + final ChatOptions options; + final DateFormatter dateFormatter; + final String currentUserId; + + @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!, + ) + : null, + ); + } + var otherUser = chat.users.firstWhere( + (element) => element.id != 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 + : 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, + ); + } +} + +Future _deleteDialog( + ChatModel chat, + ChatTranslations translations, + BuildContext context, +) async { + var theme = Theme.of(context); + + return showModalBottomSheet( + context: context, + builder: (BuildContext context) => Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + translations.deleteChatModalTitle, + textAlign: TextAlign.center, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 20, + ), + Text( + translations.deleteChatModalDescription, + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium, + ), + const SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 60), + child: FilledButton( + onPressed: () { + Navigator.of( + context, + ).pop(true); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translations.deleteChatModalConfirm, + style: theme.textTheme.displayLarge, + ), + ], + ), + ), + ), + ], + ), + ), + ); +} + +class _ChatRow extends StatelessWidget { + const _ChatRow({ + required this.title, + this.unreadMessages = 0, + this.lastUsed, + this.subTitle, + this.avatar, + super.key, + }); + + /// The title of the chat. + final String title; + + /// The number of unread messages in the chat. + final int unreadMessages; + + /// The last time the chat was used. + final String? lastUsed; + + /// The subtitle of the chat. + final String? subTitle; + + /// The avatar associated with the chat. + final Widget? avatar; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: avatar, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleMedium, + ), + if (subTitle != null) ...[ + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: Text( + subTitle!, + style: unreadMessages > 0 + ? theme.textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w800, + ) + : theme.textTheme.bodySmall, + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ), + ], + ], + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (lastUsed != null) ...[ + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + lastUsed!, + style: theme.textTheme.labelSmall, + ), + ), + ], + if (unreadMessages > 0) ...[ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + unreadMessages.toString(), + style: const TextStyle( + fontSize: 14, + ), + ), + ), + ), + ], + ], + ), + ], + ); + } +} 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 new file mode 100644 index 0000000..e883c1a --- /dev/null +++ b/packages/flutter_chat/lib/src/screens/creation/new_chat_screen.dart @@ -0,0 +1,240 @@ +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'; + +class NewChatScreen extends StatefulWidget { + const NewChatScreen({ + required this.userId, + required this.chatService, + required this.chatOptions, + required this.onPressCreateGroupChat, + required this.onPressCreateChat, + super.key, + }); + + final String userId; + final ChatService chatService; + final ChatOptions chatOptions; + final VoidCallback onPressCreateGroupChat; + final Function(UserModel) onPressCreateChat; + + @override + State createState() => _NewChatScreenState(); +} + +class _NewChatScreenState extends State { + final FocusNode _textFieldFocusNode = FocusNode(); + bool _isSearching = false; + String query = ""; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return widget.chatOptions.builders.newChatScreenScaffoldBuilder?.call( + _AppBar( + chatOptions: widget.chatOptions, + isSearching: _isSearching, + onSearch: (query) { + setState(() { + _isSearching = query.isNotEmpty; + this.query = query; + }); + }, + onPressedSearchIcon: () { + setState(() { + _isSearching = !_isSearching; + query = ""; + }); + + if (_isSearching) { + _textFieldFocusNode.requestFocus(); + } + }, + focusNode: _textFieldFocusNode, + ) as AppBar, + _Body( + chatOptions: widget.chatOptions, + chatService: widget.chatService, + isSearching: _isSearching, + onPressCreateGroupChat: widget.onPressCreateGroupChat, + onPressCreateChat: widget.onPressCreateChat, + userId: widget.userId, + query: query, + ), + theme.scaffoldBackgroundColor, + ) ?? + Scaffold( + appBar: _AppBar( + chatOptions: widget.chatOptions, + isSearching: _isSearching, + onSearch: (query) { + setState(() { + _isSearching = query.isNotEmpty; + this.query = query; + }); + }, + onPressedSearchIcon: () { + setState(() { + _isSearching = !_isSearching; + query = ""; + }); + + if (_isSearching) { + _textFieldFocusNode.requestFocus(); + } + }, + focusNode: _textFieldFocusNode, + ), + body: _Body( + chatOptions: widget.chatOptions, + chatService: widget.chatService, + isSearching: _isSearching, + onPressCreateGroupChat: widget.onPressCreateGroupChat, + onPressCreateChat: widget.onPressCreateChat, + userId: widget.userId, + query: query, + ), + ); + } +} + +class _AppBar extends StatelessWidget implements PreferredSizeWidget { + const _AppBar({ + required this.chatOptions, + required this.isSearching, + required this.onSearch, + required this.onPressedSearchIcon, + required this.focusNode, + }); + + final ChatOptions chatOptions; + final bool isSearching; + final Function(String) onSearch; + final VoidCallback onPressedSearchIcon; + final FocusNode focusNode; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return AppBar( + iconTheme: theme.appBarTheme.iconTheme ?? + const IconThemeData(color: Colors.white), + title: SearchField( + chatOptions: chatOptions, + isSearching: isSearching, + onSearch: onSearch, + focusNode: focusNode, + text: chatOptions.translations.newChatTitle, + ), + actions: [ + SearchIcon( + isSearching: isSearching, + onPressed: onPressedSearchIcon, + ), + ], + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +class _Body extends StatelessWidget { + const _Body({ + required this.chatOptions, + required this.chatService, + required this.isSearching, + required this.onPressCreateGroupChat, + required this.onPressCreateChat, + required this.userId, + required this.query, + }); + + final ChatOptions chatOptions; + final ChatService chatService; + final bool isSearching; + + final String userId; + final String query; + + final VoidCallback onPressCreateGroupChat; + final Function(UserModel) onPressCreateChat; + + @override + Widget build(BuildContext context) { + var translations = chatOptions.translations; + var theme = Theme.of(context); + + return Column( + children: [ + if (chatOptions.groupChatEnabled && !isSearching) ...[ + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 20, + ), + child: FilledButton( + onPressed: onPressCreateGroupChat, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.groups, + ), + const SizedBox( + width: 4, + ), + Text( + translations.newGroupChatButton, + style: theme.textTheme.displayLarge, + ), + ], + ), + ), + ), + ], + Expanded( + child: StreamBuilder>( + // ignore: discarded_futures + stream: chatService.getAllUsers(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Text("Error: ${snapshot.error}"); + } else if (snapshot.hasData) { + return UserList( + users: snapshot.data!, + currentUser: userId, + query: query, + options: chatOptions, + onPressCreateChat: onPressCreateChat, + ); + } else { + return chatOptions.builders.noUsersPlaceholderBuilder + ?.call(translations) ?? + Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Align( + alignment: Alignment.topCenter, + child: Text( + translations.noUsersFound, + textAlign: TextAlign.center, + style: theme.textTheme.bodySmall, + ), + ), + ); + } + }, + ), + ), + ], + ); + } +} 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 new file mode 100644 index 0000000..c4309ac --- /dev/null +++ b/packages/flutter_chat/lib/src/screens/creation/new_group_chat_overview.dart @@ -0,0 +1,395 @@ +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'; + +class NewGroupChatOverview extends StatelessWidget { + const NewGroupChatOverview({ + super.key, + required this.options, + required this.users, + required this.onComplete, + }); + + final ChatOptions options; + final List users; + 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( + _AppBar( + options: options, + ) as AppBar, + _Body( + options: options, + users: users, + onComplete: onComplete, + ), + theme.scaffoldBackgroundColor, + ) ?? + Scaffold( + appBar: _AppBar( + options: options, + ), + body: _Body( + options: options, + users: users, + onComplete: onComplete, + ), + ); + } +} + +class _AppBar extends StatelessWidget implements PreferredSizeWidget { + const _AppBar({ + required this.options, + }); + + final ChatOptions options; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return AppBar( + iconTheme: theme.appBarTheme.iconTheme ?? + const IconThemeData(color: Colors.white), + backgroundColor: theme.appBarTheme.backgroundColor, + title: Text( + options.translations.newGroupChatTitle, + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +class _Body extends StatefulWidget { + const _Body({ + required this.options, + required this.users, + required this.onComplete, + }); + + final ChatOptions options; + final List users; + final Function(List users, String chatName, String description, + Uint8List? image) onComplete; + + @override + State<_Body> createState() => _BodyState(); +} + +class _BodyState extends State<_Body> { + final TextEditingController _chatNameController = TextEditingController(); + final TextEditingController _bioController = TextEditingController(); + Uint8List? image; + + var formKey = GlobalKey(); + var isPressed = false; + + var users = []; + + @override + void initState() { + users = widget.users; + super.initState(); + } + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var translations = widget.options.translations; + return Stack( + children: [ + SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 40, + ), + Center( + child: Stack( + children: [ + GestureDetector( + onTap: () async => onPressSelectImage( + context, + widget.options, + (image) { + setState(() { + this.image = image; + }); + }, + ), + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: const Color(0xFFD9D9D9), + borderRadius: BorderRadius.circular(40), + image: image != null + ? DecorationImage( + image: MemoryImage(image!), + fit: BoxFit.cover, + ) + : null, + ), + child: + image == null ? const Icon(Icons.image) : null, + ), + ), + if (image != null) + Positioned.directional( + textDirection: Directionality.of(context), + end: 0, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: const Color(0xFFBCBCBC), + borderRadius: BorderRadius.circular(40), + ), + child: Center( + child: GestureDetector( + onTap: () { + setState(() { + image = null; + }); + }, + child: const Icon( + Icons.close, + size: 12, + ), + ), + ), + ), + ) + else + const SizedBox.shrink(), + ], + ), + ), + const SizedBox( + height: 40, + ), + Text( + translations.groupChatNameFieldHeader, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 12, + ), + TextFormField( + style: theme.textTheme.bodySmall, + controller: _chatNameController, + decoration: InputDecoration( + fillColor: Colors.white, + filled: true, + hintText: translations.groupNameHintText, + hintStyle: theme.textTheme.bodyMedium!.copyWith( + color: + theme.textTheme.bodyMedium!.color!.withOpacity(0.5), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: Colors.transparent, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: Colors.transparent, + ), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return translations.groupNameValidatorEmpty; + } + if (value.length > 15) { + return translations.groupNameValidatorTooLong; + } + + return null; + }, + ), + const SizedBox( + height: 16, + ), + Text( + translations.groupBioFieldHeader, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 12, + ), + TextFormField( + style: theme.textTheme.bodySmall, + controller: _bioController, + minLines: null, + maxLines: 5, + decoration: InputDecoration( + fillColor: Colors.white, + filled: true, + hintText: translations.groupBioHintText, + hintStyle: theme.textTheme.bodyMedium!.copyWith( + color: + theme.textTheme.bodyMedium!.color!.withOpacity(0.5), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: Colors.transparent, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: Colors.transparent, + ), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return translations.groupBioValidatorEmpty; + } + + return null; + }, + ), + const SizedBox( + height: 16, + ), + Text( + "${translations.selectedMembersHeader}" + "${users.length}", + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 12, + ), + Wrap( + children: [ + ...users.map( + (e) => _SelectedUser( + user: e, + options: widget.options, + onRemove: (user) { + setState(() { + users.remove(user); + }); + }, + ), + ), + ], + ), + const SizedBox( + height: 80, + ), + ], + ), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 80, + ), + child: FilledButton( + onPressed: users.isNotEmpty + ? () async { + if (!isPressed) { + isPressed = true; + if (formKey.currentState!.validate()) { + await widget.onComplete( + users, + _chatNameController.text, + _bioController.text, + image, + ); + } + isPressed = false; + } + } + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translations.createGroupChatButton, + style: theme.textTheme.displayLarge, + ), + ], + ), + ), + ), + ), + ], + ); + } +} + +class _SelectedUser extends StatelessWidget { + const _SelectedUser({ + required this.user, + required this.options, + required this.onRemove, + }); + + final UserModel user; + final ChatOptions options; + 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, + ), + size: 40, + ), + ), + 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 new file mode 100644 index 0000000..9dbee2d --- /dev/null +++ b/packages/flutter_chat/lib/src/screens/creation/new_group_chat_screen.dart @@ -0,0 +1,283 @@ +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'; + +class NewGroupChatScreen extends StatefulWidget { + const NewGroupChatScreen({ + required this.userId, + required this.chatService, + required this.chatOptions, + required this.onContinue, + super.key, + }); + + final String userId; + final ChatService chatService; + final ChatOptions chatOptions; + final Function(List) onContinue; + + @override + State createState() => _NewGroupChatScreenState(); +} + +class _NewGroupChatScreenState extends State { + final FocusNode _textFieldFocusNode = FocusNode(); + bool _isSearching = false; + String query = ""; + + List selectedUsers = []; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return widget.chatOptions.builders.newGroupChatScreenScaffoldBuilder?.call( + _AppBar( + chatOptions: widget.chatOptions, + isSearching: _isSearching, + onSearch: (query) { + setState(() { + _isSearching = query.isNotEmpty; + this.query = query; + }); + }, + onPressedSearchIcon: () { + setState(() { + _isSearching = !_isSearching; + query = ""; + }); + + if (_isSearching) { + _textFieldFocusNode.requestFocus(); + } + }, + focusNode: _textFieldFocusNode, + ) as AppBar, + _Body( + onSelectedUser: handleUserTap, + selectedUsers: selectedUsers, + onPressGroupChatOverview: widget.onContinue, + chatOptions: widget.chatOptions, + chatService: widget.chatService, + isSearching: _isSearching, + userId: widget.userId, + query: query, + ), + theme.scaffoldBackgroundColor, + ) ?? + Scaffold( + appBar: _AppBar( + chatOptions: widget.chatOptions, + isSearching: _isSearching, + onSearch: (query) { + setState(() { + _isSearching = query.isNotEmpty; + this.query = query; + }); + }, + onPressedSearchIcon: () { + setState(() { + _isSearching = !_isSearching; + query = ""; + }); + + if (_isSearching) { + _textFieldFocusNode.requestFocus(); + } + }, + focusNode: _textFieldFocusNode, + ), + body: _Body( + onSelectedUser: handleUserTap, + selectedUsers: selectedUsers, + onPressGroupChatOverview: widget.onContinue, + chatOptions: widget.chatOptions, + chatService: widget.chatService, + isSearching: _isSearching, + userId: widget.userId, + query: query, + ), + ); + } + + void handleUserTap(UserModel user) { + if (selectedUsers.contains(user)) { + setState(() { + selectedUsers.remove(user); + }); + } else { + setState(() { + selectedUsers.add(user); + }); + } + } +} + +class _AppBar extends StatelessWidget implements PreferredSizeWidget { + const _AppBar({ + required this.chatOptions, + required this.isSearching, + required this.onSearch, + required this.onPressedSearchIcon, + required this.focusNode, + }); + + final ChatOptions chatOptions; + final bool isSearching; + final Function(String) onSearch; + final VoidCallback onPressedSearchIcon; + final FocusNode focusNode; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return AppBar( + iconTheme: theme.appBarTheme.iconTheme ?? + const IconThemeData(color: Colors.white), + title: SearchField( + chatOptions: chatOptions, + isSearching: isSearching, + onSearch: onSearch, + focusNode: focusNode, + text: chatOptions.translations.newGroupChatTitle, + ), + actions: [ + SearchIcon( + isSearching: isSearching, + onPressed: onPressedSearchIcon, + ), + ], + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +class _Body extends StatelessWidget { + const _Body({ + required this.chatOptions, + required this.chatService, + required this.isSearching, + required this.userId, + required this.query, + required this.selectedUsers, + required this.onSelectedUser, + required this.onPressGroupChatOverview, + }); + + final ChatOptions chatOptions; + final ChatService chatService; + final bool isSearching; + + final String userId; + final String query; + + final List selectedUsers; + final Function(UserModel) onSelectedUser; + final Function(List) onPressGroupChatOverview; + + @override + Widget build(BuildContext context) { + var translations = chatOptions.translations; + var theme = Theme.of(context); + + return Column( + children: [ + Expanded( + child: StreamBuilder>( + // ignore: discarded_futures + stream: chatService.getAllUsers(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Text("Error: ${snapshot.error}"); + } else if (snapshot.hasData) { + return Stack( + children: [ + UserList( + users: snapshot.data!, + currentUser: userId, + query: query, + options: chatOptions, + onPressCreateChat: null, + creatingGroup: true, + selectedUsers: selectedUsers, + onSelectedUser: onSelectedUser, + ), + _NextButton( + selectedUsers: selectedUsers, + onPressGroupChatOverview: onPressGroupChatOverview, + chatOptions: chatOptions, + ), + ], + ); + } else { + return chatOptions.builders.noUsersPlaceholderBuilder + ?.call(translations) ?? + Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Align( + alignment: Alignment.topCenter, + child: Text( + translations.noUsersFound, + textAlign: TextAlign.center, + style: theme.textTheme.bodySmall, + ), + ), + ); + } + }, + ), + ), + ], + ); + } +} + +class _NextButton extends StatelessWidget { + const _NextButton({ + required this.onPressGroupChatOverview, + required this.selectedUsers, + required this.chatOptions, + }); + + final Function(List) onPressGroupChatOverview; + final List selectedUsers; + final ChatOptions chatOptions; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 80, + ), + child: FilledButton( + onPressed: selectedUsers.isNotEmpty + ? () { + onPressGroupChatOverview(selectedUsers); + } + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + chatOptions.translations.next, + style: theme.textTheme.displayLarge, + ), + ], + ), + ), + ), + ); + } +} 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 new file mode 100644 index 0000000..32b2bff --- /dev/null +++ b/packages/flutter_chat/lib/src/screens/creation/widgets/image_picker.dart @@ -0,0 +1,78 @@ +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'; + +Future onPressSelectImage( + BuildContext context, + ChatOptions options, + Function(Uint8List image) onUploadImage, +) async => + showModalBottomSheet( + context: context, + builder: (BuildContext context) => + options.builders.imagePickerContainerBuilder?.call( + () => Navigator.of(context).pop(), + options.translations, + context, + ) ?? + Container( + padding: const EdgeInsets.all(8.0), + color: Colors.white, + child: ImagePicker( + imagePickerTheme: ImagePickerTheme( + title: options.translations.imagePickerTitle, + titleTextSize: 16, + titleAlignment: TextAlign.center, + iconSize: 60.0, + makePhotoText: options.translations.takePicture, + selectImageText: options.translations.uploadFile, + selectImageIcon: const Icon( + Icons.insert_drive_file_rounded, + size: 60, + ), + ), + customButton: TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + options.translations.cancelImagePickerBtn, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + decoration: TextDecoration.underline, + ), + ), + ), + ), + ), + ).then( + (image) async { + if (image == null) return; + var messenger = ScaffoldMessenger.of(context) + ..showSnackBar( + _getImageLoadingSnackbar(options.translations), + ) + ..activate(); + await onUploadImage(image); + Future.delayed(const Duration(seconds: 1), () { + messenger.hideCurrentSnackBar(); + }); + }, + ); + +SnackBar _getImageLoadingSnackbar(ChatTranslations translations) => SnackBar( + duration: const Duration(minutes: 1), + content: Row( + children: [ + const SizedBox( + width: 25, + height: 25, + child: CircularProgressIndicator(color: Colors.grey), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text(translations.imageUploading), + ), + ], + ), + ); 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 new file mode 100644 index 0000000..064471f --- /dev/null +++ b/packages/flutter_chat/lib/src/screens/creation/widgets/search_field.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_chat/src/config/chat_options.dart'; + +class SearchField extends StatelessWidget { + const SearchField({ + super.key, + required this.chatOptions, + required this.isSearching, + required this.onSearch, + required this.focusNode, + required this.text, + }); + + final ChatOptions chatOptions; + final bool isSearching; + final Function(String query) onSearch; + final FocusNode focusNode; + final String text; + + @override + Widget build(BuildContext context) { + 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, + ), + ), + ), + style: theme.textTheme.bodySmall!.copyWith(color: Colors.white), + cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white, + ) + : 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 new file mode 100644 index 0000000..2a3cee9 --- /dev/null +++ b/packages/flutter_chat/lib/src/screens/creation/widgets/search_icon.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class SearchIcon extends StatelessWidget { + const SearchIcon({ + super.key, + required this.isSearching, + required this.onPressed, + }); + + final bool isSearching; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return IconButton( + onPressed: onPressed, + icon: Icon( + isSearching ? Icons.close : Icons.search, + color: theme.appBarTheme.iconTheme?.color ?? Colors.white, + ), + ); + } +} 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 new file mode 100644 index 0000000..4558c4a --- /dev/null +++ b/packages/flutter_chat/lib/src/screens/creation/widgets/user_list.dart @@ -0,0 +1,182 @@ +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'; + +class UserList extends StatefulWidget { + const UserList({ + super.key, + required this.users, + required this.currentUser, + required this.query, + required this.options, + required this.onPressCreateChat, + this.creatingGroup = false, + this.selectedUsers = const [], + this.onSelectedUser, + }); + + final List users; + final String query; + final String currentUser; + final ChatOptions options; + final bool creatingGroup; + final Function(UserModel)? onPressCreateChat; + final List selectedUsers; + final Function(UserModel)? onSelectedUser; + + @override + State createState() => _UserListState(); +} + +class _UserListState extends State { + List users = []; + List filteredUsers = []; + bool isPressed = false; + + @override + void initState() { + super.initState(); + users = List.from(widget.users); + users.removeWhere((user) => user.id == widget.currentUser); + } + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var translations = widget.options.translations; + filteredUsers = users + .where( + (user) => + user.fullname?.toLowerCase().contains( + widget.query.toLowerCase(), + ) ?? + false, + ) + .toList(); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + child: ListView.builder( + itemCount: filteredUsers.length, + itemBuilder: (context, index) { + var user = filteredUsers[index]; + var isSelected = widget.selectedUsers.any((u) => u.id == user.id); + + return InkWell( + onTap: () async { + if (widget.creatingGroup) { + return handleGroupChatTap(user); + } else { + return handlePersonalChatTap(user); + } + }, + child: widget.options.builders.chatRowContainerBuilder?.call( + Row( + children: [ + widget.options.builders.userAvatarBuilder + ?.call(user, 44) ?? + Avatar( + boxfit: BoxFit.cover, + user: User( + firstName: user.firstName, + lastName: user.lastName, + imageUrl: + user.imageUrl != "" ? user.imageUrl : null, + ), + size: 44, + ), + const SizedBox( + width: 12, + ), + Text( + user.fullname ?? translations.anonymousUser, + style: theme.textTheme.titleMedium, + ), + if (widget.creatingGroup) ...[ + const Spacer(), + Checkbox( + value: isSelected, + onChanged: (value) { + handleGroupChatTap(user); + }, + ), + const SizedBox( + width: 12, + ), + ], + ], + ), + ) ?? + DecoratedBox( + decoration: BoxDecoration( + color: Colors.transparent, + border: Border( + bottom: BorderSide( + color: theme.dividerColor, + width: 0.5, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + widget.options.builders.userAvatarBuilder + ?.call(user, 44) ?? + Avatar( + boxfit: BoxFit.cover, + user: User( + firstName: user.firstName, + lastName: user.lastName, + imageUrl: + user.imageUrl != "" ? user.imageUrl : null, + ), + size: 44, + ), + const SizedBox( + width: 12, + ), + Text( + user.fullname ?? translations.anonymousUser, + style: theme.textTheme.titleMedium, + ), + if (widget.creatingGroup) ...[ + const Spacer(), + Checkbox( + value: isSelected, + onChanged: (value) { + handleGroupChatTap(user); + }, + ), + const SizedBox( + width: 12, + ), + ], + ], + ), + ), + ), + ); + }, + ), + ); + } + + void handlePersonalChatTap(UserModel user) async { + if (!isPressed) { + setState(() { + isPressed = true; + }); + + await widget.onPressCreateChat?.call(user); + + setState(() { + isPressed = false; + }); + } + } + + void handleGroupChatTap(UserModel user) { + widget.onSelectedUser?.call(user); + } +} diff --git a/packages/flutter_chat_view/lib/src/services/date_formatter.dart b/packages/flutter_chat/lib/src/services/date_formatter.dart similarity index 96% rename from packages/flutter_chat_view/lib/src/services/date_formatter.dart rename to packages/flutter_chat/lib/src/services/date_formatter.dart index e1c4352..dac9e28 100644 --- a/packages/flutter_chat_view/lib/src/services/date_formatter.dart +++ b/packages/flutter_chat/lib/src/services/date_formatter.dart @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: BSD-3-Clause -import "package:flutter_chat_view/flutter_chat_view.dart"; +import "package:flutter_chat/src/config/chat_options.dart"; import "package:intl/intl.dart"; class DateFormatter { diff --git a/packages/flutter_chat/pubspec.yaml b/packages/flutter_chat/pubspec.yaml index 177bdfa..cdfcede 100644 --- a/packages/flutter_chat/pubspec.yaml +++ b/packages/flutter_chat/pubspec.yaml @@ -1,36 +1,68 @@ -# SPDX-FileCopyrightText: 2022 Iconica -# -# SPDX-License-Identifier: GPL-3.0-or-later - name: flutter_chat -description: A new Flutter package project. -version: 3.1.0 - +description: "A new Flutter package project." +version: 0.0.1 +homepage: publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub - environment: - sdk: ">=3.1.0 <4.0.0" + sdk: '>=3.4.3 <4.0.0' flutter: ">=1.17.0" dependencies: flutter: sdk: flutter - go_router: ^14.2.1 - flutter_chat_view: - hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub - version: ^3.1.0 - flutter_chat_interface: - hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub - version: ^3.1.0 - flutter_chat_local: - hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub - version: ^3.1.0 - uuid: ^4.3.3 + + 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 + url: https://github.com/Iconica-Development/flutter_profile + chat_repository_interface: + path: ../chat_repository_interface dev_dependencies: - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 + flutter_test: + sdk: flutter + flutter_lints: ^3.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 diff --git a/packages/flutter_chat_firebase/analysis_options.yaml b/packages/flutter_chat_firebase/analysis_options.yaml deleted file mode 100644 index 31b4b51..0000000 --- a/packages/flutter_chat_firebase/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -include: package:flutter_iconica_analysis/analysis_options.yaml - -# Possible to overwrite the rules from the package - -analyzer: - exclude: - -linter: - rules: diff --git a/packages/flutter_chat_firebase/lib/config/firebase_chat_options.dart b/packages/flutter_chat_firebase/lib/config/firebase_chat_options.dart deleted file mode 100644 index a56eb09..0000000 --- a/packages/flutter_chat_firebase/lib/config/firebase_chat_options.dart +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter/material.dart"; - -/// Options for Firebase chat configuration. -@immutable -class FirebaseChatOptions { - /// Creates a new instance of `FirebaseChatOptions`. - const FirebaseChatOptions({ - this.groupChatsCollectionName = "group_chats", - this.chatsCollectionName = "chats", - this.messagesCollectionName = "messages", - this.usersCollectionName = "users", - this.chatsMetaDataCollectionName = "chat_metadata", - this.userChatsCollectionName = "chats", - }); - - /// The collection name for group chats. - final String groupChatsCollectionName; - - /// The collection name for chats. - final String chatsCollectionName; - - /// The collection name for messages. - final String messagesCollectionName; - - /// The collection name for users. - final String usersCollectionName; - - /// The collection name for chat metadata. - final String chatsMetaDataCollectionName; - - /// The collection name for user chats. - final String userChatsCollectionName; - - /// Creates a copy of this FirebaseChatOptions but with the given fields - /// replaced with the new values. - FirebaseChatOptions copyWith({ - String? groupChatsCollectionName, - String? chatsCollectionName, - String? messagesCollectionName, - String? usersCollectionName, - String? chatsMetaDataCollectionName, - String? userChatsCollectionName, - }) => - FirebaseChatOptions( - groupChatsCollectionName: - groupChatsCollectionName ?? this.groupChatsCollectionName, - chatsCollectionName: chatsCollectionName ?? this.chatsCollectionName, - messagesCollectionName: - messagesCollectionName ?? this.messagesCollectionName, - usersCollectionName: usersCollectionName ?? this.usersCollectionName, - chatsMetaDataCollectionName: - chatsMetaDataCollectionName ?? this.chatsMetaDataCollectionName, - userChatsCollectionName: - userChatsCollectionName ?? this.userChatsCollectionName, - ); -} diff --git a/packages/flutter_chat_firebase/lib/dto/firebase_chat_document.dart b/packages/flutter_chat_firebase/lib/dto/firebase_chat_document.dart deleted file mode 100644 index 721b67e..0000000 --- a/packages/flutter_chat_firebase/lib/dto/firebase_chat_document.dart +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:cloud_firestore/cloud_firestore.dart"; -import "package:flutter/material.dart"; -import "package:flutter_chat_firebase/dto/firebase_message_document.dart"; - -/// Represents a chat document in Firebase. -@immutable -class FirebaseChatDocument { - /// Creates a new instance of `FirebaseChatDocument`. - const FirebaseChatDocument({ - required this.personal, - required this.canBeDeleted, - this.users = const [], - this.id, - this.lastUsed, - this.title, - this.imageUrl, - this.lastMessage, - this.bio, - }); - - /// Constructs a FirebaseChatDocument from JSON. - FirebaseChatDocument.fromJson(Map json, this.id) - : title = json["title"], - imageUrl = json["image_url"], - personal = json["personal"], - canBeDeleted = json["can_be_deleted"] ?? true, - lastUsed = json["last_used"], - users = json["users"] != null ? List.from(json["users"]) : [], - lastMessage = json["last_message"] == null - ? null - : FirebaseMessageDocument.fromJson( - json["last_message"], - null, - ), - bio = json["bio"]; - - /// The unique identifier of the chat document. - final String? id; - - /// The title of the chat. - final String? title; - - /// The image URL of the chat. - final String? imageUrl; - - /// Indicates if the chat is personal. - final bool personal; - - /// Indicates if the chat can be deleted. - final bool canBeDeleted; - - /// The timestamp of when the chat was last used. - final Timestamp? lastUsed; - - /// The list of users participating in the chat. - final List users; - - /// The last message in the chat. - final FirebaseMessageDocument? lastMessage; - - final String? bio; - - /// Converts the FirebaseChatDocument to JSON format. - Map toJson() => { - "title": title, - "image_url": imageUrl, - "personal": personal, - "last_used": lastUsed, - "can_be_deleted": canBeDeleted, - "users": users, - "bio": bio, - }; -} diff --git a/packages/flutter_chat_firebase/lib/dto/firebase_message_document.dart b/packages/flutter_chat_firebase/lib/dto/firebase_message_document.dart deleted file mode 100644 index 1d374cf..0000000 --- a/packages/flutter_chat_firebase/lib/dto/firebase_message_document.dart +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:cloud_firestore/cloud_firestore.dart"; -import "package:flutter/material.dart"; - -/// Represents a message document in Firebase. -@immutable -class FirebaseMessageDocument { - /// Creates a new instance of `FirebaseMessageDocument`. - const FirebaseMessageDocument({ - required this.sender, - required this.timestamp, - this.id, - this.text, - this.imageUrl, - }); - - /// Constructs a FirebaseMessageDocument from JSON. - FirebaseMessageDocument.fromJson(Map json, this.id) - : sender = json["sender"], - text = json["text"], - imageUrl = json["image_url"], - timestamp = json["timestamp"]; - - /// The unique identifier of the message document. - final String? id; - - /// The sender of the message. - final String sender; - - /// The text content of the message. - final String? text; - - /// The image URL of the message. - final String? imageUrl; - - /// The timestamp of when the message was sent. - final Timestamp timestamp; - - /// Converts the FirebaseMessageDocument to JSON format. - Map toJson() => { - "sender": sender, - "text": text, - "image_url": imageUrl, - "timestamp": timestamp, - }; -} diff --git a/packages/flutter_chat_firebase/lib/dto/firebase_user_document.dart b/packages/flutter_chat_firebase/lib/dto/firebase_user_document.dart deleted file mode 100644 index 94455df..0000000 --- a/packages/flutter_chat_firebase/lib/dto/firebase_user_document.dart +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter/material.dart"; - -/// Represents a user document in Firebase. -@immutable -class FirebaseUserDocument { - /// Creates a new instance of `FirebaseUserDocument`. - const FirebaseUserDocument({ - this.firstName, - this.lastName, - this.imageUrl, - this.id, - }); - - /// Constructs a FirebaseUserDocument from JSON. - FirebaseUserDocument.fromJson( - Map json, - String id, - ) : this( - id: id, - firstName: - json["first_name"] == null ? "" : json["first_name"]! as String, - lastName: - json["last_name"] == null ? "" : json["last_name"]! as String, - imageUrl: - json["image_url"] == null ? null : json["image_url"]! as String, - ); - - /// The first name of the user. - final String? firstName; - - /// The last name of the user. - final String? lastName; - - /// The image URL of the user. - final String? imageUrl; - - /// The unique identifier of the user document. - final String? id; - - /// Converts the FirebaseUserDocument to JSON format. - Map toJson() => { - "first_name": firstName, - "last_name": lastName, - "image_url": imageUrl, - }; -} diff --git a/packages/flutter_chat_firebase/lib/flutter_chat_firebase.dart b/packages/flutter_chat_firebase/lib/flutter_chat_firebase.dart deleted file mode 100644 index 1c13b92..0000000 --- a/packages/flutter_chat_firebase/lib/flutter_chat_firebase.dart +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause -/// -library flutter_chat_firebase; - -export "package:flutter_chat_firebase/service/service.dart"; diff --git a/packages/flutter_chat_firebase/lib/service/firebase_chat_detail_service.dart b/packages/flutter_chat_firebase/lib/service/firebase_chat_detail_service.dart deleted file mode 100644 index 3e9dad6..0000000 --- a/packages/flutter_chat_firebase/lib/service/firebase_chat_detail_service.dart +++ /dev/null @@ -1,346 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "dart:async"; -import "dart:typed_data"; -import "package:cloud_firestore/cloud_firestore.dart"; -import "package:firebase_core/firebase_core.dart"; -import "package:firebase_storage/firebase_storage.dart"; -import "package:flutter/material.dart"; -import "package:flutter_chat_firebase/config/firebase_chat_options.dart"; -import "package:flutter_chat_firebase/dto/firebase_message_document.dart"; -import "package:flutter_chat_interface/flutter_chat_interface.dart"; -import "package:uuid/uuid.dart"; - -/// Service class for managing chat details using Firebase. -class FirebaseChatDetailService - with ChangeNotifier - implements ChatDetailService { - /// Constructor for FirebaseChatDetailService. - /// - /// [userService]: Instance of ChatUserService. - /// [app]: Optional FirebaseApp instance, defaults to Firebase.app(). - /// [options]: Optional FirebaseChatOptions instance, - /// defaults to FirebaseChatOptions(). - FirebaseChatDetailService({ - required ChatUserService userService, - FirebaseApp? app, - FirebaseChatOptions? options, - }) { - var appInstance = app ?? Firebase.app(); - - _db = FirebaseFirestore.instanceFor(app: appInstance); - _storage = FirebaseStorage.instanceFor(app: appInstance); - _userService = userService; - _options = options ?? const FirebaseChatOptions(); - } - late final FirebaseFirestore _db; - late final FirebaseStorage _storage; - late final ChatUserService _userService; - late FirebaseChatOptions _options; - - StreamController>? _controller; - StreamSubscription? _subscription; - DocumentSnapshot? lastMessage; - List _cumulativeMessages = []; - String? lastChat; - int? chatPageSize; - DateTime timestampToFilter = DateTime.now(); - - Future _sendMessage(String chatId, Map data) async { - var currentUser = await _userService.getCurrentUser(); - - if (currentUser == null) { - return; - } - - var message = { - "sender": currentUser.id, - "timestamp": DateTime.now(), - ...data, - }; - - var chatReference = _db - .collection( - _options.chatsCollectionName, - ) - .doc(chatId); - - var newMessage = await chatReference - .collection( - _options.messagesCollectionName, - ) - .add(message); - - if (_cumulativeMessages.length == 1) { - lastMessage = await chatReference - .collection( - _options.messagesCollectionName, - ) - .doc(newMessage.id) - .get(); - } - - var metadataReference = _db - .collection( - _options.chatsMetaDataCollectionName, - ) - .doc(chatId); - - await metadataReference.update({ - "last_used": DateTime.now(), - "last_message": message, - }); - - // update the chat counter for the other users - // get all users from the chat - // there is a field in the chat document called users that has a - // list of user ids - var fetchedChat = await metadataReference.get(); - var chatUsers = fetchedChat.data()?["users"] as List; - // for all users except the message sender update the unread counter - for (var userId in chatUsers) { - if (userId != currentUser.id) { - var userReference = _db - .collection( - _options.usersCollectionName, - ) - .doc(userId) - .collection(_options.userChatsCollectionName) - .doc(chatId); - // what if the amount_unread_messages field does not exist? - // it should be created when the chat is create - if ((await userReference.get()) - .data() - ?.containsKey("amount_unread_messages") ?? - false) { - await userReference.update({ - "amount_unread_messages": FieldValue.increment(1), - }); - } else { - await userReference.set( - { - "amount_unread_messages": 1, - }, - SetOptions(merge: true), - ); - } - } - } - } - - /// Sends a text message to a chat. - /// - /// [text]: The text message to send. - /// [chatId]: The ID of the chat where the message will be sent. - @override - Future sendTextMessage({ - required String text, - required String chatId, - }) => - _sendMessage( - chatId, - { - "text": text, - }, - ); - - /// Sends an image message to a chat. - /// - /// [chatId]: The ID of the chat where the message will be sent. - /// [image]: The image data to send. - @override - Future sendImageMessage({ - required String chatId, - required Uint8List image, - }) async { - var ref = _storage - .ref("${_options.chatsCollectionName}/$chatId/${const Uuid().v4()}"); - - return ref.putData(image).then( - (_) => ref.getDownloadURL().then( - (url) { - _sendMessage( - chatId, - { - "image_url": url, - }, - ); - }, - ), - ); - } - - /// Retrieves a stream of messages for a chat. - /// - /// [chatId]: The ID of the chat. - @override - Stream> getMessagesStream(String chatId) { - timestampToFilter = DateTime.now(); - var messages = []; - _controller = StreamController>( - onListen: () { - var messagesCollection = _db - .collection(_options.chatsCollectionName) - .doc(chatId) - .collection(_options.messagesCollectionName) - .where( - "timestamp", - isGreaterThan: timestampToFilter, - ) - .withConverter( - fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson( - snapshot.data()!, - snapshot.id, - ), - toFirestore: (user, _) => user.toJson(), - ) - .snapshots(); - - _subscription = messagesCollection.listen((event) async { - for (var message in event.docChanges) { - var data = message.doc.data(); - var sender = await _userService.getUser(data!.sender); - var timestamp = DateTime.fromMillisecondsSinceEpoch( - data.timestamp.millisecondsSinceEpoch, - ); - - if (timestamp.isBefore(timestampToFilter)) { - return; - } - messages.add( - data.imageUrl != null - ? ChatImageMessageModel( - sender: sender!, - imageUrl: data.imageUrl!, - timestamp: timestamp, - ) - : ChatTextMessageModel( - sender: sender!, - text: data.text!, - timestamp: timestamp, - ), - ); - timestampToFilter = DateTime.now(); - } - _cumulativeMessages = [ - ..._cumulativeMessages, - ...messages, - ]; - var uniqueObjects = _cumulativeMessages.toSet().toList(); - _cumulativeMessages = uniqueObjects; - _cumulativeMessages - .sort((a, b) => a.timestamp.compareTo(b.timestamp)); - notifyListeners(); - }); - }, - onCancel: () async { - await _subscription?.cancel(); - _subscription = null; - _cumulativeMessages = []; - lastChat = chatId; - lastMessage = null; - }, - ); - - return _controller!.stream; - } - - /// Stops listening for messages. - @override - Future stopListeningForMessages() async { - await _subscription?.cancel(); - _subscription = null; - await _controller?.close(); - _controller = null; - } - - /// Fetches more messages for a chat. - /// - /// [pageSize]: The number of messages to fetch. - /// [chatId]: The ID of the chat. - @override - Future fetchMoreMessage( - int pageSize, - String chatId, - ) async { - if (lastChat != chatId) { - _cumulativeMessages = []; - lastChat = chatId; - lastMessage = null; - } - - // get the x amount of last messages from the oldest message that is in - // cumulative messages and add that to the list - var messages = []; - QuerySnapshot? messagesQuerySnapshot; - var query = _db - .collection(_options.chatsCollectionName) - .doc(chatId) - .collection(_options.messagesCollectionName) - .orderBy("timestamp", descending: true) - .limit(pageSize); - if (lastMessage == null) { - messagesQuerySnapshot = await query - .withConverter( - fromFirestore: (snapshot, _) => - FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id), - toFirestore: (user, _) => user.toJson(), - ) - .get(); - if (messagesQuerySnapshot.docs.isNotEmpty) { - lastMessage = messagesQuerySnapshot.docs.last; - } - } else { - messagesQuerySnapshot = await query - .startAfterDocument(lastMessage!) - .withConverter( - fromFirestore: (snapshot, _) => - FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id), - toFirestore: (user, _) => user.toJson(), - ) - .get(); - if (messagesQuerySnapshot.docs.isNotEmpty) { - lastMessage = messagesQuerySnapshot.docs.last; - } - } - - var messageDocuments = messagesQuerySnapshot.docs - .map((QueryDocumentSnapshot doc) => doc.data()) - .toList(); - for (var message in messageDocuments) { - var sender = await _userService.getUser(message.sender); - if (sender != null) { - var timestamp = DateTime.fromMillisecondsSinceEpoch( - message.timestamp.millisecondsSinceEpoch, - ); - - messages.add( - message.imageUrl != null - ? ChatImageMessageModel( - sender: sender, - imageUrl: message.imageUrl!, - timestamp: timestamp, - ) - : ChatTextMessageModel( - sender: sender, - text: message.text!, - timestamp: timestamp, - ), - ); - } - } - - _cumulativeMessages = [ - ...messages, - ..._cumulativeMessages, - ]; - _cumulativeMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - notifyListeners(); - } - - /// Retrieves the list of messages. - @override - List getMessages() => _cumulativeMessages; -} diff --git a/packages/flutter_chat_firebase/lib/service/firebase_chat_overview_service.dart b/packages/flutter_chat_firebase/lib/service/firebase_chat_overview_service.dart deleted file mode 100644 index d4f531e..0000000 --- a/packages/flutter_chat_firebase/lib/service/firebase_chat_overview_service.dart +++ /dev/null @@ -1,537 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "dart:async"; -import "dart:typed_data"; - -import "package:cloud_firestore/cloud_firestore.dart"; -import "package:firebase_core/firebase_core.dart"; -import "package:firebase_storage/firebase_storage.dart"; -import "package:flutter/material.dart"; -import "package:flutter_chat_firebase/config/firebase_chat_options.dart"; -import "package:flutter_chat_firebase/dto/firebase_chat_document.dart"; -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -/// Service class for managing chat overviews using Firebase. -class FirebaseChatOverviewService - with ChangeNotifier - implements ChatOverviewService { - late FirebaseFirestore _db; - late FirebaseStorage _storage; - late ChatUserService _userService; - late FirebaseChatOptions _options; - - /// Constructor for FirebaseChatOverviewService. - /// - /// [userService]: Instance of ChatUserService. - /// [app]: Optional FirebaseApp instance, defaults to Firebase.app(). - /// [options]: Optional FirebaseChatOptions instance, defaults - /// to FirebaseChatOptions(). - FirebaseChatOverviewService({ - required ChatUserService userService, - FirebaseApp? app, - FirebaseChatOptions? options, - }) { - var appInstance = app ?? Firebase.app(); - - _db = FirebaseFirestore.instanceFor(app: appInstance); - _storage = FirebaseStorage.instanceFor(app: appInstance); - _userService = userService; - _options = options ?? const FirebaseChatOptions(); - } - - final List _currentlySelectedUsers = []; - - Future _addUnreadChatSubscription( - String chatId, - String userId, - ) async { - var snapshots = await _db - .collection(_options.usersCollectionName) - .doc(userId) - .collection(_options.userChatsCollectionName) - .doc(chatId) - .get(); - - return snapshots.data()?["amount_unread_messages"]; - } - - /// Retrieves a stream of chat overviews. - @override - Stream> getChatsStream() { - StreamSubscription? chatSubscription; - // ignore: close_sinks - late StreamController> controller; - controller = StreamController( - onListen: () async { - var currentUser = await _userService.getCurrentUser(); - var userSnapshot = _db - .collection(_options.usersCollectionName) - .doc(currentUser?.id) - .collection(_options.userChatsCollectionName) - .snapshots(); - - userSnapshot.listen((event) { - var chatIds = event.docs.map((e) => e.id).toList(); - var chatSnapshot = _db - .collection(_options.chatsMetaDataCollectionName) - .where( - FieldPath.documentId, - whereIn: chatIds, - ) - .withConverter( - fromFirestore: (snapshot, _) => FirebaseChatDocument.fromJson( - snapshot.data()!, - snapshot.id, - ), - toFirestore: (chat, _) => chat.toJson(), - ) - .snapshots(); - var chats = []; - ChatModel? chatModel; - - chatSubscription = chatSnapshot.listen((event) async { - for (var element in event.docChanges) { - var chat = element.doc.data(); - if (chat == null) return; - - var otherUser = chat.users.any( - (element) => element != currentUser?.id, - ) - ? await _userService.getUser( - chat.users.firstWhere( - (element) => element != currentUser?.id, - ), - ) - : null; - - var unread = - await _addUnreadChatSubscription(chat.id!, currentUser!.id!); - - if (chat.personal) { - chatModel = PersonalChatModel( - id: chat.id, - user: otherUser!, - unreadMessages: unread, - lastUsed: chat.lastUsed == null - ? null - : DateTime.fromMillisecondsSinceEpoch( - chat.lastUsed!.millisecondsSinceEpoch, - ), - lastMessage: chat.lastMessage != null && - chat.lastMessage!.imageUrl != null - ? ChatImageMessageModel( - sender: otherUser, - imageUrl: chat.lastMessage!.imageUrl!, - timestamp: DateTime.fromMillisecondsSinceEpoch( - chat.lastMessage!.timestamp.millisecondsSinceEpoch, - ), - ) - : chat.lastMessage != null - ? ChatTextMessageModel( - sender: otherUser, - text: chat.lastMessage!.text!, - timestamp: DateTime.fromMillisecondsSinceEpoch( - chat.lastMessage!.timestamp - .millisecondsSinceEpoch, - ), - ) - : null, - ); - } else { - var users = []; - for (var userId in chat.users) { - var user = await _userService.getUser(userId); - if (user != null) { - users.add(user); - } - } - chatModel = GroupChatModel( - id: chat.id, - title: chat.title ?? "", - imageUrl: chat.imageUrl ?? "", - unreadMessages: unread, - users: users, - lastMessage: chat.lastMessage != null && otherUser != null - ? chat.lastMessage!.imageUrl == null - ? ChatTextMessageModel( - sender: otherUser, - text: chat.lastMessage!.text!, - timestamp: DateTime.fromMillisecondsSinceEpoch( - chat.lastMessage!.timestamp - .millisecondsSinceEpoch, - ), - ) - : ChatImageMessageModel( - sender: otherUser, - imageUrl: chat.lastMessage!.imageUrl!, - timestamp: DateTime.fromMillisecondsSinceEpoch( - chat.lastMessage!.timestamp - .millisecondsSinceEpoch, - ), - ) - : null, - canBeDeleted: chat.canBeDeleted, - lastUsed: chat.lastUsed == null - ? null - : DateTime.fromMillisecondsSinceEpoch( - chat.lastUsed!.millisecondsSinceEpoch, - ), - ); - } - chats.add(chatModel!); - } - var uniqueIds = {}; - var uniqueChatModels = []; - - for (var chatModel in chats) { - if (uniqueIds.add(chatModel.id!)) { - uniqueChatModels.add(chatModel); - } else { - var index = uniqueChatModels.indexWhere( - (element) => element.id == chatModel.id, - ); - if (index != -1) { - if (chatModel.lastUsed != null && - uniqueChatModels[index].lastUsed != null) { - if (chatModel.lastUsed! - .isAfter(uniqueChatModels[index].lastUsed!)) { - uniqueChatModels[index] = chatModel; - } - } - } - } - } - - uniqueChatModels.sort( - (a, b) => (b.lastUsed ?? DateTime.now()).compareTo( - a.lastUsed ?? DateTime.now(), - ), - ); - - controller.add(uniqueChatModels); - }); - }); - }, - onCancel: () async { - await chatSubscription?.cancel(); - }, - ); - return controller.stream; - } - - /// Retrieves a chat by the given user. - /// - /// [user]: The user associated with the chat. - @override - Future getChatByUser(ChatUserModel user) async { - var currentUser = await _userService.getCurrentUser(); - var collection = await _db - .collection(_options.usersCollectionName) - .doc(currentUser?.id) - .collection(_options.userChatsCollectionName) - .where("users", arrayContains: user.id) - .get(); - - var doc = collection.docs.isNotEmpty ? collection.docs.first : null; - - return PersonalChatModel( - id: doc?.id, - user: user, - ); - } - - /// Retrieves a chat by the given ID. - /// - /// [chatId]: The ID of the chat. - @override - Future getChatById(String chatId) async { - var currentUser = await _userService.getCurrentUser(); - var chatCollection = await _db - .collection(_options.usersCollectionName) - .doc(currentUser?.id) - .collection(_options.userChatsCollectionName) - .doc(chatId) - .get(); - - if (chatCollection.exists && chatCollection.data()?["users"] != null) { - // ignore: avoid_dynamic_calls - var otherUser = chatCollection.data()?["users"].firstWhere( - (element) => element != currentUser?.id, - ); - var user = await _userService.getUser(otherUser); - return PersonalChatModel( - id: chatId, - user: user!, - canBeDeleted: chatCollection.data()?["can_be_deleted"] ?? true, - ); - } else { - var groupChatCollection = await _db - .collection(_options.chatsMetaDataCollectionName) - .doc(chatId) - .withConverter( - fromFirestore: (snapshot, _) => - FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id), - toFirestore: (chat, _) => chat.toJson(), - ) - .get(); - var chat = groupChatCollection.data(); - var users = []; - for (var userId in chat?.users ?? []) { - var user = await _userService.getUser(userId); - if (user != null) { - users.add(user); - } - } - return GroupChatModel( - id: chat?.id ?? chatId, - title: chat?.title ?? "", - imageUrl: chat?.imageUrl ?? "", - users: users, - canBeDeleted: chat?.canBeDeleted ?? true, - bio: chat?.bio, - ); - } - } - - /// Deletes the given chat. - /// - /// [chat]: The chat to be deleted. - @override - Future deleteChat(ChatModel chat) async { - var chatCollection = await _db - .collection(_options.chatsMetaDataCollectionName) - .doc(chat.id) - .withConverter( - fromFirestore: (snapshot, _) => - FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id), - toFirestore: (chat, _) => chat.toJson(), - ) - .get(); - - var chatData = chatCollection.data(); - - if (chatData != null) { - for (var userId in chatData.users) { - await _db - .collection(_options.usersCollectionName) - .doc(userId) - .collection(_options.userChatsCollectionName) - .doc(chat.id) - .delete(); - } - - if (chat.id != null) { - await _db - .collection(_options.chatsCollectionName) - .doc(chat.id) - .delete(); - await _storage - .ref(_options.chatsCollectionName) - .child(chat.id!) - .listAll() - .then((value) { - for (var element in value.items) { - element.delete(); - } - }); - } - } - } - - /// Stores the given chat if it does not exist already. - /// - /// [chat]: The chat to be stored. - @override - Future storeChatIfNot(ChatModel chat, Uint8List? image) async { - if (chat.id == null) { - var currentUser = await _userService.getCurrentUser(); - if (chat is PersonalChatModel) { - if (currentUser?.id == null || chat.user.id == null) { - return chat; - } - - var userIds = [ - currentUser!.id!, - chat.user.id!, - ]; - - var reference = await _db - .collection(_options.chatsMetaDataCollectionName) - .withConverter( - fromFirestore: (snapshot, _) => - FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id), - toFirestore: (chat, _) => chat.toJson(), - ) - .add( - FirebaseChatDocument( - personal: true, - canBeDeleted: chat.canBeDeleted, - users: userIds, - lastUsed: Timestamp.now(), - ), - ); - - for (var userId in userIds) { - await _db - .collection(_options.usersCollectionName) - .doc(userId) - .collection(_options.userChatsCollectionName) - .doc(reference.id) - .set({"users": userIds}, SetOptions(merge: true)); - } - - chat.id = reference.id; - } else if (chat is GroupChatModel) { - if (currentUser?.id == null) { - return chat; - } - - var userIds = [ - currentUser!.id!, - ...chat.users.map((e) => e.id!), - ]; - - var reference = await _db - .collection(_options.chatsMetaDataCollectionName) - .withConverter( - fromFirestore: (snapshot, _) => - FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id), - toFirestore: (chat, _) => chat.toJson(), - ) - .add( - FirebaseChatDocument( - personal: false, - title: chat.title, - canBeDeleted: chat.canBeDeleted, - users: userIds, - lastUsed: Timestamp.now(), - bio: chat.bio, - ), - ); - - if (image != null) { - var imageUrl = await uploadGroupChatImage(image, reference.id); - chat.copyWith(imageUrl: imageUrl); - await _db - .collection(_options.chatsMetaDataCollectionName) - .doc(reference.id) - .set({"image_url": imageUrl}, SetOptions(merge: true)); - } - var currentChat = await _db - .collection(_options.chatsMetaDataCollectionName) - .doc(reference.id) - .withConverter( - fromFirestore: (snapshot, _) => - FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id), - toFirestore: (chat, _) => chat.toJson(), - ) - .get(); - - for (var userId in userIds) { - await _db - .collection(_options.usersCollectionName) - .doc(userId) - .collection(_options.groupChatsCollectionName) - .doc(currentChat.id) - .set({"users": userIds}, SetOptions(merge: true)); - } - chat.id = reference.id; - currentlySelectedUsers.clear(); - } else { - throw Exception("Chat type not supported for firebase"); - } - } - - return chat; - } - - /// Retrieves a stream of the count of unread chats. - @override - Stream getUnreadChatsCountStream() { - // open a stream to the user's chats collection and listen to changes in - // this collection we will also add the amount of read chats - StreamSubscription? unreadChatSubscription; - // ignore: close_sinks - late StreamController controller; - controller = StreamController( - onListen: () async { - var currentUser = await _userService.getCurrentUser(); - var userSnapshot = _db - .collection(_options.usersCollectionName) - .doc(currentUser?.id) - .collection(_options.userChatsCollectionName) - .snapshots(); - - unreadChatSubscription = userSnapshot.listen((event) { - // every chat has a field called amount_unread_messages, combine all - // of these fields to get the total amount of unread messages - var unreadChats = event.docs - .map((chat) => chat.data()["amount_unread_messages"] ?? 0) - .toList(); - var totalUnreadChats = unreadChats.fold( - 0, - (previousValue, element) => previousValue + (element as int), - ); - controller.add(totalUnreadChats); - }); - }, - onCancel: () async { - await unreadChatSubscription?.cancel(); - }, - ); - return controller.stream; - } - - /// Marks a chat as read. - /// - /// [chat]: The chat to be marked as read. - @override - Future readChat(ChatModel chat) async { - // set the amount of read chats to the amount of messages in the chat - var currentUser = await _userService.getCurrentUser(); - if (currentUser?.id == null || chat.id == null) { - return; - } - // set the amount of unread messages to 0 - - await _db - .collection(_options.usersCollectionName) - .doc(currentUser!.id) - .collection(_options.userChatsCollectionName) - .doc(chat.id) - .set({"amount_unread_messages": 0}, SetOptions(merge: true)); - } - - @override - List get currentlySelectedUsers => _currentlySelectedUsers; - - @override - void addCurrentlySelectedUser(ChatUserModel user) { - _currentlySelectedUsers.add(user); - notifyListeners(); - } - - @override - void removeCurrentlySelectedUser(ChatUserModel user) { - _currentlySelectedUsers.remove(user); - notifyListeners(); - } - - @override - Future uploadGroupChatImage(Uint8List image, String chatId) async { - await _storage.ref("groupchatImages/$chatId").putData(image); - var imageUrl = - await _storage.ref("groupchatImages/$chatId").getDownloadURL(); - - return imageUrl; - } - - @override - void clearCurrentlySelectedUsers() { - _currentlySelectedUsers.clear(); - notifyListeners(); - } -} diff --git a/packages/flutter_chat_firebase/lib/service/firebase_chat_service.dart b/packages/flutter_chat_firebase/lib/service/firebase_chat_service.dart deleted file mode 100644 index bc564a6..0000000 --- a/packages/flutter_chat_firebase/lib/service/firebase_chat_service.dart +++ /dev/null @@ -1,85 +0,0 @@ -import "package:firebase_core/firebase_core.dart"; -import "package:flutter_chat_firebase/config/firebase_chat_options.dart"; -import "package:flutter_chat_firebase/flutter_chat_firebase.dart"; -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -/// Service class for managing chat services using Firebase. -class FirebaseChatService implements ChatService { - FirebaseChatService({ - this.options, - this.app, - this.firebaseChatDetailService, - this.firebaseChatOverviewService, - this.firebaseChatUserService, - }) { - firebaseChatDetailService ??= FirebaseChatDetailService( - userService: chatUserService, - options: options, - app: app, - ); - - firebaseChatOverviewService ??= FirebaseChatOverviewService( - userService: chatUserService, - options: options, - app: app, - ); - - firebaseChatUserService ??= FirebaseChatUserService( - options: options, - app: app, - ); - } - - /// The options for configuring Firebase Chat. - final FirebaseChatOptions? options; - - /// The Firebase app instance. - final FirebaseApp? app; - - /// The service for managing chat details. - ChatDetailService? firebaseChatDetailService; - - /// The service for managing chat overviews. - ChatOverviewService? firebaseChatOverviewService; - - /// The service for managing chat users. - ChatUserService? firebaseChatUserService; - - @override - ChatDetailService get chatDetailService { - if (firebaseChatDetailService != null) { - return firebaseChatDetailService!; - } else { - return FirebaseChatDetailService( - userService: chatUserService, - options: options, - app: app, - ); - } - } - - @override - ChatOverviewService get chatOverviewService { - if (firebaseChatOverviewService != null) { - return firebaseChatOverviewService!; - } else { - return FirebaseChatOverviewService( - userService: chatUserService, - options: options, - app: app, - ); - } - } - - @override - ChatUserService get chatUserService { - if (firebaseChatUserService != null) { - return firebaseChatUserService!; - } else { - return FirebaseChatUserService( - options: options, - app: app, - ); - } - } -} diff --git a/packages/flutter_chat_firebase/lib/service/firebase_chat_user_service.dart b/packages/flutter_chat_firebase/lib/service/firebase_chat_user_service.dart deleted file mode 100644 index 56c1f9f..0000000 --- a/packages/flutter_chat_firebase/lib/service/firebase_chat_user_service.dart +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:cloud_firestore/cloud_firestore.dart"; -import "package:firebase_auth/firebase_auth.dart"; -import "package:firebase_core/firebase_core.dart"; -import "package:flutter_chat_firebase/config/firebase_chat_options.dart"; -import "package:flutter_chat_firebase/dto/firebase_user_document.dart"; -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -/// Service class for managing chat users using Firebase. -class FirebaseChatUserService implements ChatUserService { - /// Constructor for FirebaseChatUserService. - /// - /// [app]: The Firebase app instance. - /// [options]: The options for configuring Firebase Chat. - FirebaseChatUserService({ - FirebaseApp? app, - FirebaseChatOptions? options, - }) { - var appInstance = app ?? Firebase.app(); - - _db = FirebaseFirestore.instanceFor(app: appInstance); - _auth = FirebaseAuth.instanceFor(app: appInstance); - _options = options ?? const FirebaseChatOptions(); - } - - /// The Firebase Firestore instance. - late FirebaseFirestore _db; - - /// The Firebase Authentication instance. - late FirebaseAuth _auth; - - /// The options for configuring Firebase Chat. - late FirebaseChatOptions _options; - - /// The current user. - ChatUserModel? _currentUser; - - /// Map to cache user models. - final Map _users = {}; - - /// Collection reference for users. - CollectionReference get _userCollection => _db - .collection(_options.usersCollectionName) - .withConverter( - fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson( - snapshot.data()!, - snapshot.id, - ), - toFirestore: (user, _) => user.toJson(), - ); - - @override - Future getUser(String id) async { - if (_users.containsKey(id)) { - return _users[id]!; - } - - return _userCollection.doc(id).get().then((response) { - var data = response.data(); - - var user = data == null - ? ChatUserModel(id: id) - : ChatUserModel( - id: id, - firstName: data.firstName, - lastName: data.lastName, - imageUrl: data.imageUrl, - ); - - _users[id] = user; - - return user; - }); - } - - @override - Future getCurrentUser() async => - _currentUser == null && _auth.currentUser?.uid != null - ? _currentUser = await getUser(_auth.currentUser!.uid) - : _currentUser; - - @override - Future> getAllUsers() async { - var currentUser = await getCurrentUser(); - - var query = _userCollection.where( - FieldPath.documentId, - isNotEqualTo: currentUser?.id, - ); - - var data = await query.get(); - - return data.docs.map((user) { - var userData = user.data(); - return ChatUserModel( - id: user.id, - firstName: userData.firstName, - lastName: userData.lastName, - imageUrl: userData.imageUrl, - ); - }).toList(); - } -} diff --git a/packages/flutter_chat_firebase/lib/service/service.dart b/packages/flutter_chat_firebase/lib/service/service.dart deleted file mode 100644 index 5f13d58..0000000 --- a/packages/flutter_chat_firebase/lib/service/service.dart +++ /dev/null @@ -1,4 +0,0 @@ -export "package:flutter_chat_firebase/service/firebase_chat_detail_service.dart"; -export "package:flutter_chat_firebase/service/firebase_chat_overview_service.dart"; -export "package:flutter_chat_firebase/service/firebase_chat_service.dart"; -export "package:flutter_chat_firebase/service/firebase_chat_user_service.dart"; diff --git a/packages/flutter_chat_firebase/pubspec.yaml b/packages/flutter_chat_firebase/pubspec.yaml deleted file mode 100644 index 2ca2ea4..0000000 --- a/packages/flutter_chat_firebase/pubspec.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Iconica -# -# SPDX-License-Identifier: GPL-3.0-or-later - -name: flutter_chat_firebase -description: A new Flutter package project. -version: 3.1.0 - -publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub - -environment: - sdk: ">=3.1.0 <4.0.0" - flutter: ">=1.17.0" - -dependencies: - flutter: - sdk: flutter - firebase_core: ^2.1.1 - cloud_firestore: ^4.0.5 - firebase_storage: ^11.0.5 - firebase_auth: ^4.1.2 - uuid: ^4.0.0 - flutter_chat_interface: - hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub - version: ^3.1.0 - -dev_dependencies: - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 - -flutter: diff --git a/packages/flutter_chat_interface/analysis_options.yaml b/packages/flutter_chat_interface/analysis_options.yaml deleted file mode 100644 index 31b4b51..0000000 --- a/packages/flutter_chat_interface/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -include: package:flutter_iconica_analysis/analysis_options.yaml - -# Possible to overwrite the rules from the package - -analyzer: - exclude: - -linter: - rules: diff --git a/packages/flutter_chat_interface/lib/flutter_chat_interface.dart b/packages/flutter_chat_interface/lib/flutter_chat_interface.dart deleted file mode 100644 index a490142..0000000 --- a/packages/flutter_chat_interface/lib/flutter_chat_interface.dart +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause -/// -library flutter_chat_interface; - -export "package:flutter_chat_interface/src/chat_data_provider.dart"; -export "package:flutter_chat_interface/src/model/model.dart"; -export "package:flutter_chat_interface/src/service/service.dart"; diff --git a/packages/flutter_chat_interface/lib/src/chat_data_provider.dart b/packages/flutter_chat_interface/lib/src/chat_data_provider.dart deleted file mode 100644 index 3e01507..0000000 --- a/packages/flutter_chat_interface/lib/src/chat_data_provider.dart +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter_chat_interface/flutter_chat_interface.dart"; -import "package:flutter_data_interface/flutter_data_interface.dart"; - -class ChatDataProvider extends DataInterface { - ChatDataProvider({ - required this.chatService, - required this.userService, - required this.messageService, - }) : super(token: _token); - - static final Object _token = Object(); - final ChatUserService userService; - final ChatOverviewService chatService; - final ChatDetailService messageService; -} diff --git a/packages/flutter_chat_interface/lib/src/model/chat.dart b/packages/flutter_chat_interface/lib/src/model/chat.dart deleted file mode 100644 index 5a5e1f2..0000000 --- a/packages/flutter_chat_interface/lib/src/model/chat.dart +++ /dev/null @@ -1,77 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -abstract class ChatModelInterface { - ChatModelInterface copyWith(); - String? get id; - List? get messages; - int? get unreadMessages; - DateTime? get lastUsed; - ChatMessageModel? get lastMessage; - bool get canBeDeleted; -} - -/// A concrete implementation of [ChatModelInterface] representing a chat. -class ChatModel implements ChatModelInterface { - /// Constructs a [ChatModel] instance. - /// - /// [id]: The ID of the chat. - /// - /// [messages]: The list of messages in the chat. - /// - /// [unreadMessages]: The number of unread messages in the chat. - /// - /// [lastUsed]: The timestamp when the chat was last used. - /// - /// [lastMessage]: The last message sent in the chat. - /// - /// [canBeDeleted]: Indicates whether the chat can be deleted. - ChatModel({ - this.id, - this.messages = const [], - this.unreadMessages, - this.lastUsed, - this.lastMessage, - this.canBeDeleted = true, - }); - - @override - String? id; - - @override - final List? messages; - - @override - final int? unreadMessages; - - @override - final DateTime? lastUsed; - - @override - final ChatMessageModel? lastMessage; - - @override - final bool canBeDeleted; - - @override - ChatModel copyWith({ - String? id, - List? messages, - int? unreadMessages, - DateTime? lastUsed, - ChatMessageModel? lastMessage, - bool? canBeDeleted, - }) => - ChatModel( - id: id ?? this.id, - messages: messages ?? this.messages, - unreadMessages: unreadMessages ?? this.unreadMessages, - lastUsed: lastUsed ?? this.lastUsed, - lastMessage: lastMessage ?? this.lastMessage, - canBeDeleted: canBeDeleted ?? this.canBeDeleted, - ); -} diff --git a/packages/flutter_chat_interface/lib/src/model/chat_image_message.dart b/packages/flutter_chat_interface/lib/src/model/chat_image_message.dart deleted file mode 100644 index 54f3225..0000000 --- a/packages/flutter_chat_interface/lib/src/model/chat_image_message.dart +++ /dev/null @@ -1,48 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -/// An abstract class defining the interface for an image message in a chat. -abstract class ChatImageMessageModelInterface extends ChatMessageModel { - /// Constructs a [ChatImageMessageModelInterface] instance. - /// - /// [sender]: The sender of the message. - /// - /// [timestamp]: The timestamp when the message was sent. - ChatImageMessageModelInterface({ - required super.sender, - required super.timestamp, - }); - - /// Returns the URL of the image associated with the message. - String get imageUrl; -} - -/// A concrete implementation of [ChatImageMessageModelInterface] -/// representing an image message in a chat. -class ChatImageMessageModel implements ChatImageMessageModelInterface { - /// Constructs a [ChatImageMessageModel] instance. - /// - /// [sender]: The sender of the message. - /// - /// [timestamp]: The timestamp when the message was sent. - /// - /// [imageUrl]: The URL of the image associated with the message. - ChatImageMessageModel({ - required this.sender, - required this.timestamp, - required this.imageUrl, - }); - - @override - final ChatUserModel sender; - - @override - final DateTime timestamp; - - @override - final String imageUrl; -} diff --git a/packages/flutter_chat_interface/lib/src/model/chat_message.dart b/packages/flutter_chat_interface/lib/src/model/chat_message.dart deleted file mode 100644 index ebc1b10..0000000 --- a/packages/flutter_chat_interface/lib/src/model/chat_message.dart +++ /dev/null @@ -1,31 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter_chat_interface/src/model/chat_user.dart"; - -abstract class ChatMessageModelInterface { - ChatUserModel get sender; - DateTime get timestamp; -} - -/// A concrete implementation of [ChatMessageModelInterface] -/// representing a chat message. -class ChatMessageModel implements ChatMessageModelInterface { - /// Constructs a [ChatMessageModel] instance. - /// - /// [sender]: The sender of the message. - /// - /// [timestamp]: The timestamp when the message was sent. - ChatMessageModel({ - required this.sender, - required this.timestamp, - }); - - @override - final ChatUserModel sender; - - @override - final DateTime timestamp; -} diff --git a/packages/flutter_chat_interface/lib/src/model/chat_text_message.dart b/packages/flutter_chat_interface/lib/src/model/chat_text_message.dart deleted file mode 100644 index 73a8fd3..0000000 --- a/packages/flutter_chat_interface/lib/src/model/chat_text_message.dart +++ /dev/null @@ -1,41 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -abstract class ChatTextMessageModelInterface extends ChatMessageModel { - ChatTextMessageModelInterface({ - required super.sender, - required super.timestamp, - }); - - String get text; -} - -/// A concrete implementation of [ChatTextMessageModelInterface] -/// representing a text message in a chat. -class ChatTextMessageModel implements ChatTextMessageModelInterface { - /// Constructs a [ChatTextMessageModel] instance. - /// - /// [sender]: The sender of the message. - /// - /// [timestamp]: The timestamp when the message was sent. - /// - /// [text]: The text content of the message. - ChatTextMessageModel({ - required this.sender, - required this.timestamp, - required this.text, - }); - - @override - final ChatUserModel sender; - - @override - final DateTime timestamp; - - @override - final String text; -} diff --git a/packages/flutter_chat_interface/lib/src/model/chat_user.dart b/packages/flutter_chat_interface/lib/src/model/chat_user.dart deleted file mode 100644 index 129f7d8..0000000 --- a/packages/flutter_chat_interface/lib/src/model/chat_user.dart +++ /dev/null @@ -1,72 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter/material.dart"; - -abstract class ChatUserModelInterface { - String? get id; - String? get firstName; - String? get lastName; - String? get imageUrl; - - String? get fullName; -} - -/// A concrete implementation of [ChatUserModelInterface] -/// representing a chat user. -@immutable -class ChatUserModel implements ChatUserModelInterface { - /// Constructs a [ChatUserModel] instance. - /// - /// [id]: The ID of the user. - /// - /// [firstName]: The first name of the user. - /// - /// [lastName]: The last name of the user. - /// - /// [imageUrl]: The URL of the user's image. - /// - const ChatUserModel({ - this.id, - this.firstName, - this.lastName, - this.imageUrl, - }); - - @override - final String? id; - - @override - final String? firstName; - - @override - final String? lastName; - - @override - final String? imageUrl; - - @override - String? get fullName { - var fullName = ""; - - if (firstName != null && lastName != null) { - fullName += "$firstName $lastName"; - } else if (firstName != null) { - fullName += firstName!; - } else if (lastName != null) { - fullName += lastName!; - } - - return fullName == "" ? null : fullName; - } - - @override - bool operator ==(Object other) => - identical(this, other) || other is ChatUserModel && id == other.id; - - @override - int get hashCode => - id.hashCode ^ firstName.hashCode ^ lastName.hashCode ^ imageUrl.hashCode; -} diff --git a/packages/flutter_chat_interface/lib/src/model/group_chat.dart b/packages/flutter_chat_interface/lib/src/model/group_chat.dart deleted file mode 100644 index 84f56ca..0000000 --- a/packages/flutter_chat_interface/lib/src/model/group_chat.dart +++ /dev/null @@ -1,117 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -abstract class GroupChatModelInterface extends ChatModel { - GroupChatModelInterface({ - super.id, - super.messages, - super.lastUsed, - super.lastMessage, - super.unreadMessages, - super.canBeDeleted, - }); - - String get title; - String? get imageUrl; - List get users; - String? get bio; - - @override - GroupChatModelInterface copyWith({ - String? id, - List? messages, - int? unreadMessages, - DateTime? lastUsed, - ChatMessageModel? lastMessage, - String? title, - String? imageUrl, - List? users, - bool? canBeDeleted, - String? bio, - }); -} - -class GroupChatModel implements GroupChatModelInterface { - /// Constructs a [GroupChatModel] instance. - /// - /// [id]: The ID of the chat. - /// - /// [messages]: The list of messages in the chat. - /// - /// [unreadMessages]: The number of unread messages in the chat. - /// - /// [lastUsed]: The timestamp when the chat was last used. - /// - /// [lastMessage]: The last message sent in the chat. - /// - /// [title]: The title of the group chat. - /// - /// [imageUrl]: The URL of the image associated with the group chat. - /// - /// [users]: The list of users participating in the group chat. - /// - /// [canBeDeleted]: Indicates whether the chat can be deleted. - GroupChatModel({ - required this.canBeDeleted, - required this.title, - required this.users, - this.imageUrl, - this.id, - this.messages, - this.unreadMessages, - this.lastUsed, - this.lastMessage, - this.bio, - }); - - @override - String? id; - @override - final List? messages; - @override - final int? unreadMessages; - @override - final DateTime? lastUsed; - @override - final ChatMessageModel? lastMessage; - @override - final bool canBeDeleted; - @override - final String title; - @override - final String? imageUrl; - @override - final List users; - @override - final String? bio; - - @override - GroupChatModel copyWith({ - String? id, - List? messages, - int? unreadMessages, - DateTime? lastUsed, - ChatMessageModel? lastMessage, - bool? canBeDeleted, - String? title, - String? imageUrl, - List? users, - String? bio, - }) => - GroupChatModel( - id: id ?? this.id, - messages: messages ?? this.messages, - unreadMessages: unreadMessages ?? this.unreadMessages, - lastUsed: lastUsed ?? this.lastUsed, - lastMessage: lastMessage ?? this.lastMessage, - canBeDeleted: canBeDeleted ?? this.canBeDeleted, - title: title ?? this.title, - imageUrl: imageUrl ?? this.imageUrl, - users: users ?? this.users, - bio: bio ?? this.bio, - ); -} diff --git a/packages/flutter_chat_interface/lib/src/model/model.dart b/packages/flutter_chat_interface/lib/src/model/model.dart deleted file mode 100644 index c11c337..0000000 --- a/packages/flutter_chat_interface/lib/src/model/model.dart +++ /dev/null @@ -1,7 +0,0 @@ -export "chat.dart"; -export "chat_image_message.dart"; -export "chat_message.dart"; -export "chat_text_message.dart"; -export "chat_user.dart"; -export "group_chat.dart"; -export "personal_chat.dart"; diff --git a/packages/flutter_chat_interface/lib/src/model/personal_chat.dart b/packages/flutter_chat_interface/lib/src/model/personal_chat.dart deleted file mode 100644 index 0c907de..0000000 --- a/packages/flutter_chat_interface/lib/src/model/personal_chat.dart +++ /dev/null @@ -1,93 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -abstract class PersonalChatModelInterface extends ChatModel { - PersonalChatModelInterface({ - super.id, - super.messages, - super.unreadMessages, - super.lastUsed, - super.lastMessage, - super.canBeDeleted, - }); - - ChatUserModel get user; - - @override - PersonalChatModel copyWith({ - String? id, - List? messages, - int? unreadMessages, - DateTime? lastUsed, - ChatMessageModel? lastMessage, - ChatUserModel? user, - bool? canBeDeleted, - }); -} - -class PersonalChatModel implements PersonalChatModelInterface { - /// Constructs a [PersonalChatModel] instance. - /// - /// [user]: The user involved in the personal chat. - /// - /// [id]: The ID of the chat. - /// - /// [messages]: The list of messages in the chat. - /// - /// [unreadMessages]: The number of unread messages in the chat. - /// - /// [lastUsed]: The timestamp when the chat was last used. - /// - /// [lastMessage]: The last message sent in the chat. - /// - /// [canBeDeleted]: Indicates whether the chat can be deleted. - PersonalChatModel({ - required this.user, - this.id, - this.messages = const [], - this.unreadMessages, - this.lastUsed, - this.lastMessage, - this.canBeDeleted = true, - }); - - @override - String? id; - @override - final List? messages; - @override - final int? unreadMessages; - @override - final DateTime? lastUsed; - @override - final ChatMessageModel? lastMessage; - @override - final bool canBeDeleted; - - @override - final ChatUserModel user; - - @override - PersonalChatModel copyWith({ - String? id, - List? messages, - int? unreadMessages, - DateTime? lastUsed, - ChatMessageModel? lastMessage, - bool? canBeDeleted, - ChatUserModel? user, - }) => - PersonalChatModel( - id: id ?? this.id, - messages: messages ?? this.messages, - unreadMessages: unreadMessages ?? this.unreadMessages, - lastUsed: lastUsed ?? this.lastUsed, - lastMessage: lastMessage ?? this.lastMessage, - user: user ?? this.user, - canBeDeleted: canBeDeleted ?? this.canBeDeleted, - ); -} diff --git a/packages/flutter_chat_interface/lib/src/service/chat_detail_service.dart b/packages/flutter_chat_interface/lib/src/service/chat_detail_service.dart deleted file mode 100644 index b3d77ae..0000000 --- a/packages/flutter_chat_interface/lib/src/service/chat_detail_service.dart +++ /dev/null @@ -1,34 +0,0 @@ -import "dart:typed_data"; -import "package:flutter/material.dart"; -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -/// An abstract class defining the interface for a chat detail service. -abstract class ChatDetailService with ChangeNotifier { - /// Sends a text message to the specified chat. - Future sendTextMessage({ - required String chatId, - required String text, - }); - - /// Sends an image message to the specified chat. - Future sendImageMessage({ - required String chatId, - required Uint8List image, - }); - - /// Retrieves a stream of messages for the specified chat. - Stream> getMessagesStream( - String chatId, - ); - - Future fetchMoreMessage( - int pageSize, - String chatId, - ); - - /// Retrieves the list of messages for the chat. - List getMessages(); - - /// Stops listening for messages. - void stopListeningForMessages(); -} diff --git a/packages/flutter_chat_interface/lib/src/service/chat_overview_service.dart b/packages/flutter_chat_interface/lib/src/service/chat_overview_service.dart deleted file mode 100644 index effc9ea..0000000 --- a/packages/flutter_chat_interface/lib/src/service/chat_overview_service.dart +++ /dev/null @@ -1,42 +0,0 @@ -import "dart:typed_data"; - -import "package:flutter/material.dart"; -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -abstract class ChatOverviewService extends ChangeNotifier { - /// Retrieves a stream of chats. - /// This stream is updated whenever a new chat is created. - Stream> getChatsStream(); - - /// Retrieves a chat based on the user. - Future getChatByUser(ChatUserModel user); - - /// Retrieves a chat based on the ID. - Future getChatById(String id); - - /// Deletes the chat for this user and the other users in the chat. - Future deleteChat(ChatModel chat); - - /// When a chat is read, this method is called. - Future readChat(ChatModel chat); - - /// Creates the chat if it does not exist. - Future storeChatIfNot(ChatModel chat, Uint8List? image); - - /// Retrieves the number of unread chats. - Stream getUnreadChatsCountStream(); - - /// Retrieves the currently selected users to be added to a new groupchat. - List get currentlySelectedUsers; - - /// Adds a user to the currently selected users. - void addCurrentlySelectedUser(ChatUserModel user); - - /// Deletes a user from the currently selected users. - void removeCurrentlySelectedUser(ChatUserModel user); - - void clearCurrentlySelectedUsers(); - - /// uploads an image for a group chat. - Future uploadGroupChatImage(Uint8List image, String chatId); -} diff --git a/packages/flutter_chat_interface/lib/src/service/chat_service.dart b/packages/flutter_chat_interface/lib/src/service/chat_service.dart deleted file mode 100644 index de8e208..0000000 --- a/packages/flutter_chat_interface/lib/src/service/chat_service.dart +++ /dev/null @@ -1,14 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -class ChatService { - final ChatUserService chatUserService; - final ChatOverviewService chatOverviewService; - final ChatDetailService chatDetailService; - - ChatService({ - required this.chatUserService, - required this.chatOverviewService, - required this.chatDetailService, - }); -} diff --git a/packages/flutter_chat_interface/lib/src/service/service.dart b/packages/flutter_chat_interface/lib/src/service/service.dart deleted file mode 100644 index 79d3772..0000000 --- a/packages/flutter_chat_interface/lib/src/service/service.dart +++ /dev/null @@ -1,4 +0,0 @@ -export "chat_detail_service.dart"; -export "chat_overview_service.dart"; -export "chat_service.dart"; -export "user_service.dart"; diff --git a/packages/flutter_chat_interface/lib/src/service/user_service.dart b/packages/flutter_chat_interface/lib/src/service/user_service.dart deleted file mode 100644 index f1daa27..0000000 --- a/packages/flutter_chat_interface/lib/src/service/user_service.dart +++ /dev/null @@ -1,13 +0,0 @@ -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -abstract class ChatUserService { - /// Retrieves a user based on the ID. - Future getUser(String id); - - /// Retrieves the current user. - /// This is the user that is currently logged in. - Future getCurrentUser(); - - /// Retrieves all users. Used for chat creation. - Future> getAllUsers(); -} diff --git a/packages/flutter_chat_interface/pubspec.yaml b/packages/flutter_chat_interface/pubspec.yaml deleted file mode 100644 index b4c452c..0000000 --- a/packages/flutter_chat_interface/pubspec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Iconica -# -# SPDX-License-Identifier: GPL-3.0-or-later - -name: flutter_chat_interface -description: A new Flutter package project. -version: 3.1.0 -publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub - -environment: - sdk: ">=3.1.0 <4.0.0" - flutter: ">=1.17.0" - -dependencies: - flutter: - sdk: flutter - flutter_data_interface: - hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub - version: ^1.0.0 - -dev_dependencies: - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 - -flutter: diff --git a/packages/flutter_chat_local/analysis_options.yaml b/packages/flutter_chat_local/analysis_options.yaml deleted file mode 100644 index 31b4b51..0000000 --- a/packages/flutter_chat_local/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -include: package:flutter_iconica_analysis/analysis_options.yaml - -# Possible to overwrite the rules from the package - -analyzer: - exclude: - -linter: - rules: diff --git a/packages/flutter_chat_local/lib/local_chat_service.dart b/packages/flutter_chat_local/lib/local_chat_service.dart deleted file mode 100644 index 49a7315..0000000 --- a/packages/flutter_chat_local/lib/local_chat_service.dart +++ /dev/null @@ -1,7 +0,0 @@ -/// -library local_chat_service; - -export "service/local_chat_detail_service.dart"; -export "service/local_chat_overview_service.dart"; -export "service/local_chat_service.dart"; -export "service/local_chat_user_service.dart"; diff --git a/packages/flutter_chat_local/lib/service/local_chat_detail_service.dart b/packages/flutter_chat_local/lib/service/local_chat_detail_service.dart deleted file mode 100644 index a20289c..0000000 --- a/packages/flutter_chat_local/lib/service/local_chat_detail_service.dart +++ /dev/null @@ -1,133 +0,0 @@ -import "dart:async"; - -import "package:flutter/foundation.dart"; -import "package:flutter_chat_interface/flutter_chat_interface.dart"; -import "package:flutter_chat_local/local_chat_service.dart"; - -/// A class providing local chat detail service implementation. -class LocalChatDetailService with ChangeNotifier implements ChatDetailService { - /// Constructs a [LocalChatDetailService] instance. - /// - /// [chatOverviewService]: The chat overview service. - LocalChatDetailService({required this.chatOverviewService}); - - /// The chat overview service. - final ChatOverviewService chatOverviewService; - - /// The list of cumulative messages. - final List _cumulativeMessages = []; - - /// The stream controller for messages. - final StreamController> _controller = - StreamController>.broadcast(); - - /// The subscription for the stream. - late StreamSubscription? _subscription; - - @override - Future fetchMoreMessage( - int pageSize, - String chatId, - ) async { - var value = await chatOverviewService.getChatById(chatId); - _cumulativeMessages.clear(); - if (value.messages != null) { - _cumulativeMessages.addAll(value.messages!); - } - _controller.add(_cumulativeMessages); - notifyListeners(); - } - - @override - List getMessages() => _cumulativeMessages; - - @override - Stream> getMessagesStream( - String chatId, - ) { - _controller.onListen = () async { - _subscription = - chatOverviewService.getChatById(chatId).asStream().listen((event) { - if (event.messages != null) { - _cumulativeMessages.clear(); - _cumulativeMessages.addAll(event.messages!); - _controller.add(_cumulativeMessages); - } - }); - }; - - return _controller.stream; - } - - @override - Future sendImageMessage({ - required String chatId, - required Uint8List image, - }) async { - var chat = (chatOverviewService as LocalChatOverviewService) - .chats - .firstWhere((element) => element.id == chatId); - var message = ChatImageMessageModel( - sender: const ChatUserModel( - id: "3", - firstName: "ico", - lastName: "nica", - imageUrl: "https://picsum.photos/100/200", - ), - timestamp: DateTime.now(), - imageUrl: "https://picsum.photos/200/300", - ); - - await (chatOverviewService as LocalChatOverviewService).updateChat( - chat.copyWith( - messages: [...?chat.messages, message], - lastMessage: message, - lastUsed: DateTime.now(), - ), - ); - chat.messages?.add(message); - _cumulativeMessages.add(message); - notifyListeners(); - - return Future.value(); - } - - @override - Future sendTextMessage({ - required String chatId, - required String text, - }) async { - var chat = (chatOverviewService as LocalChatOverviewService) - .chats - .firstWhere((element) => element.id == chatId); - var message = ChatTextMessageModel( - sender: const ChatUserModel( - id: "3", - firstName: "ico", - lastName: "nica", - imageUrl: "https://picsum.photos/100/200", - ), - timestamp: DateTime.now(), - text: text, - ); - await (chatOverviewService as LocalChatOverviewService).updateChat( - chat.copyWith( - messages: [...?chat.messages, message], - lastMessage: message, - lastUsed: DateTime.now(), - ), - ); - - chat.messages?.add(message); - _cumulativeMessages.add(message); - notifyListeners(); - - return Future.value(); - } - - @override - Future stopListeningForMessages() async { - await _subscription?.cancel(); - _subscription = null; - } -} diff --git a/packages/flutter_chat_local/lib/service/local_chat_overview_service.dart b/packages/flutter_chat_local/lib/service/local_chat_overview_service.dart deleted file mode 100644 index 0d5ec79..0000000 --- a/packages/flutter_chat_local/lib/service/local_chat_overview_service.dart +++ /dev/null @@ -1,116 +0,0 @@ -import "dart:async"; - -import "package:flutter/foundation.dart"; -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -/// A class providing local chat overview service implementation. -class LocalChatOverviewService - with ChangeNotifier - implements ChatOverviewService { - /// The list of currently selected users. - final List _currentlySelectedUsers = []; - - /// The list of personal chat models. - final List _chats = []; - - /// Retrieves the list of personal chat models. - List get chats => _chats; - - /// The stream controller for chats. - final StreamController> _chatsController = - StreamController>.broadcast(); - - Future updateChat(ChatModel chat) { - var index = _chats.indexWhere((element) => element.id == chat.id); - _chats[index] = chat; - _chatsController.addStream(Stream.value(_chats)); - notifyListeners(); - return Future.value(); - } - - @override - Future deleteChat(ChatModel chat) { - _chats.removeWhere((element) => element.id == chat.id); - _chatsController.add(_chats); - notifyListeners(); - return Future.value(); - } - - @override - Future getChatById(String id) { - var chat = _chats.firstWhere((element) => element.id == id); - return Future.value(chat); - } - - @override - Future getChatByUser(ChatUserModel user) async { - PersonalChatModel? chat; - try { - chat = _chats - .whereType() - .firstWhere((element) => element.user.id == user.id); - // ignore: avoid_catching_errors - } on StateError { - chat = PersonalChatModel( - user: user, - messages: [], - id: "", - ); - chat.id = chat.hashCode.toString(); - _chats.add(chat); - } - - _chatsController.add([..._chats]); - notifyListeners(); - return chat; - } - - @override - Stream> getChatsStream() => _chatsController.stream; - - @override - Stream getUnreadChatsCountStream() => Stream.value(0); - - @override - Future readChat(ChatModel chat) async => Future.value(); - - @override - Future storeChatIfNot(ChatModel chat, Uint8List? image) { - var chatExists = _chats.any((element) => element.id == chat.id); - - if (!chatExists) { - chat.id = chat.hashCode.toString(); - _chats.add(chat); - _chatsController.add([..._chats]); - currentlySelectedUsers.clear(); - notifyListeners(); - } - - return Future.value(chat); - } - - @override - List get currentlySelectedUsers => _currentlySelectedUsers; - - @override - void addCurrentlySelectedUser(ChatUserModel user) { - _currentlySelectedUsers.add(user); - notifyListeners(); - } - - @override - void removeCurrentlySelectedUser(ChatUserModel user) { - _currentlySelectedUsers.remove(user); - notifyListeners(); - } - - @override - Future uploadGroupChatImage(Uint8List image, String chatId) => - Future.value("https://picsum.photos/200/300"); - - @override - void clearCurrentlySelectedUsers() { - _currentlySelectedUsers.clear(); - notifyListeners(); - } -} diff --git a/packages/flutter_chat_local/lib/service/local_chat_service.dart b/packages/flutter_chat_local/lib/service/local_chat_service.dart deleted file mode 100644 index a7097cd..0000000 --- a/packages/flutter_chat_local/lib/service/local_chat_service.dart +++ /dev/null @@ -1,46 +0,0 @@ -import "package:flutter_chat_interface/flutter_chat_interface.dart"; -import "package:flutter_chat_local/service/local_chat_detail_service.dart"; -import "package:flutter_chat_local/service/local_chat_overview_service.dart"; -import "package:flutter_chat_local/service/local_chat_user_service.dart"; - -/// Service class for managing local chat services. -class LocalChatService implements ChatService { - /// Constructor for LocalChatService. - /// - /// [localChatDetailService]: Optional local ChatDetailService instance, - /// defaults to LocalChatDetailService. - /// [localChatOverviewService]: Optional local ChatOverviewService instance, - /// defaults to LocalChatOverviewService. - /// [localChatUserService]: Optional local ChatUserService instance, - /// defaults to LocalChatUserService. - LocalChatService({ - this.localChatDetailService, - this.localChatOverviewService, - this.localChatUserService, - }) { - localChatOverviewService ??= LocalChatOverviewService(); - localChatDetailService ??= LocalChatDetailService( - chatOverviewService: localChatOverviewService!, - ); - - localChatUserService ??= LocalChatUserService(); - } - - /// The local chat detail service. - ChatDetailService? localChatDetailService; - - /// The local chat overview service. - ChatOverviewService? localChatOverviewService; - - /// The local chat user service. - ChatUserService? localChatUserService; - - @override - ChatDetailService get chatDetailService => localChatDetailService!; - - @override - ChatOverviewService get chatOverviewService => localChatOverviewService!; - - @override - ChatUserService get chatUserService => localChatUserService!; -} diff --git a/packages/flutter_chat_local/lib/service/local_chat_user_service.dart b/packages/flutter_chat_local/lib/service/local_chat_user_service.dart deleted file mode 100644 index 34c4809..0000000 --- a/packages/flutter_chat_local/lib/service/local_chat_user_service.dart +++ /dev/null @@ -1,40 +0,0 @@ -import "package:flutter_chat_interface/flutter_chat_interface.dart"; - -/// Service class for managing local chat users. -class LocalChatUserService implements ChatUserService { - /// List of predefined chat users. - List users = [ - const ChatUserModel( - id: "1", - firstName: "John", - lastName: "Doe", - imageUrl: "https://picsum.photos/200/300", - ), - const ChatUserModel( - id: "2", - firstName: "Jane", - lastName: "Doe", - imageUrl: "https://picsum.photos/200/300", - ), - const ChatUserModel( - id: "3", - firstName: "ico", - lastName: "nica", - imageUrl: "https://picsum.photos/100/200", - ), - ]; - - @override - Future> getAllUsers() => - Future.value(users.where((element) => element.id != "3").toList()); - - @override - Future getCurrentUser() => - Future.value(users.where((element) => element.id == "3").first); - - @override - Future getUser(String id) { - var user = users.firstWhere((element) => element.id == id); - return Future.value(user); - } -} diff --git a/packages/flutter_chat_local/pubspec.yaml b/packages/flutter_chat_local/pubspec.yaml deleted file mode 100644 index 65e7c3c..0000000 --- a/packages/flutter_chat_local/pubspec.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: flutter_chat_local -description: "A new Flutter package project." -version: 3.1.0 - -publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub - -environment: - sdk: ">=3.2.5 <4.0.0" - flutter: ">=1.17.0" - -dependencies: - flutter: - sdk: flutter - flutter_chat_interface: - hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub - version: ^3.1.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 -flutter: diff --git a/packages/flutter_chat_view/analysis_options.yaml b/packages/flutter_chat_view/analysis_options.yaml deleted file mode 100644 index 31b4b51..0000000 --- a/packages/flutter_chat_view/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -include: package:flutter_iconica_analysis/analysis_options.yaml - -# Possible to overwrite the rules from the package - -analyzer: - exclude: - -linter: - rules: diff --git a/packages/flutter_chat_view/lib/flutter_chat_view.dart b/packages/flutter_chat_view/lib/flutter_chat_view.dart deleted file mode 100644 index 1a5fd18..0000000 --- a/packages/flutter_chat_view/lib/flutter_chat_view.dart +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause -/// -library flutter_chat_view; - -export "package:flutter_chat_interface/flutter_chat_interface.dart"; - -export "src/components/chat_row.dart"; -export "src/config/chat_options.dart"; -export "src/config/chat_text_styles.dart"; -export "src/config/chat_translations.dart"; -export "src/screens/chat_detail_screen.dart"; -export "src/screens/chat_profile_screen.dart"; -export "src/screens/chat_screen.dart"; -export "src/screens/new_chat_screen.dart"; -export "src/screens/new_group_chat_overview_screen.dart"; -export "src/screens/new_group_chat_screen.dart"; diff --git a/packages/flutter_chat_view/lib/src/components/chat_bottom.dart b/packages/flutter_chat_view/lib/src/components/chat_bottom.dart deleted file mode 100644 index e57ebb3..0000000 --- a/packages/flutter_chat_view/lib/src/components/chat_bottom.dart +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter/material.dart"; -import "package:flutter_chat_view/flutter_chat_view.dart"; - -class ChatBottom extends StatefulWidget { - const ChatBottom({ - required this.chat, - required this.onMessageSubmit, - required this.messageInputBuilder, - required this.translations, - this.onPressSelectImage, - this.iconColor, - this.iconDisabledColor, - super.key, - }); - - /// Callback function invoked when a message is submitted. - final Future Function(String text) onMessageSubmit; - - /// The builder function for the message input. - final TextInputBuilder messageInputBuilder; - - /// Callback function invoked when the select image button is pressed. - final VoidCallback? onPressSelectImage; - - /// The chat model. - final ChatModel chat; - - /// The translations for the chat. - final ChatTranslations translations; - - /// The color of the icons. - final Color? iconColor; - final Color? iconDisabledColor; - - @override - State createState() => _ChatBottomState(); -} - -class _ChatBottomState extends State { - final TextEditingController _textEditingController = TextEditingController(); - bool _isTyping = false; - bool _isSending = false; - - @override - Widget build(BuildContext context) { - _textEditingController.addListener(() { - if (_textEditingController.text.isEmpty) { - setState(() { - _isTyping = false; - }); - } else { - setState(() { - _isTyping = true; - }); - } - }); - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 16, - ), - child: SizedBox( - height: 45, - child: widget.messageInputBuilder( - _textEditingController, - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: widget.onPressSelectImage, - icon: Icon( - Icons.image_outlined, - color: widget.iconColor, - ), - ), - IconButton( - disabledColor: widget.iconDisabledColor, - color: widget.iconColor, - onPressed: _isTyping && !_isSending - ? () async { - setState(() { - _isSending = true; - }); - - var value = _textEditingController.text; - - if (value.isNotEmpty) { - await widget.onMessageSubmit(value); - _textEditingController.clear(); - } - - setState(() { - _isSending = false; - }); - } - : null, - icon: const Icon( - Icons.send, - ), - ), - ], - ), - widget.translations, - context, - ), - ), - ); - } -} diff --git a/packages/flutter_chat_view/lib/src/components/chat_detail_row.dart b/packages/flutter_chat_view/lib/src/components/chat_detail_row.dart deleted file mode 100644 index 7b10779..0000000 --- a/packages/flutter_chat_view/lib/src/components/chat_detail_row.dart +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:cached_network_image/cached_network_image.dart"; -import "package:flutter/material.dart"; -import "package:flutter_chat_view/flutter_chat_view.dart"; -import "package:flutter_chat_view/src/components/chat_image.dart"; -import "package:flutter_chat_view/src/services/date_formatter.dart"; - -class ChatDetailRow extends StatefulWidget { - const ChatDetailRow({ - required this.translations, - required this.message, - required this.userAvatarBuilder, - required this.onPressUserProfile, - required this.options, - this.usernameBuilder, - this.previousMessage, - this.showTime = false, - super.key, - }); - - /// The translations for the chat. - final ChatTranslations translations; - - /// The chat message model. - final ChatMessageModel message; - - /// The builder function for user avatar. - final UserAvatarBuilder userAvatarBuilder; - - /// The previous chat message model. - final ChatMessageModel? previousMessage; - final Function(ChatUserModel user) onPressUserProfile; - final Widget Function(String userFullName)? usernameBuilder; - - /// Flag indicating whether to show the time. - final bool showTime; - - final ChatOptions options; - - @override - State createState() => _ChatDetailRowState(); -} - -class _ChatDetailRowState extends State { - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - var dateFormatter = DateFormatter(options: widget.options); - - var isNewDate = widget.previousMessage != null && - widget.message.timestamp.day != widget.previousMessage?.timestamp.day; - var isSameSender = widget.previousMessage == null || - widget.previousMessage?.sender.id != widget.message.sender.id; - var isSameMinute = widget.previousMessage != null && - widget.message.timestamp.minute == - widget.previousMessage?.timestamp.minute; - var hasHeader = isNewDate || isSameSender; - return Padding( - padding: EdgeInsets.only( - top: isNewDate || isSameSender ? 25.0 : 0, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isNewDate || isSameSender) ...[ - GestureDetector( - onTap: () => widget.onPressUserProfile( - widget.message.sender, - ), - child: Padding( - padding: const EdgeInsets.only(left: 10.0), - child: widget.message.sender.imageUrl?.isNotEmpty ?? false - ? ChatImage( - image: widget.message.sender.imageUrl!, - ) - : widget.userAvatarBuilder( - widget.message.sender, - 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.usernameBuilder?.call( - widget.message.sender.fullName ?? "", - ) ?? - Text( - widget.message.sender.fullName ?? - widget.translations.anonymousUser, - style: widget - .options.textstyles?.senderTextStyle ?? - theme.textTheme.titleMedium, - ), - ), - Padding( - padding: const EdgeInsets.only(top: 5.0), - child: Text( - dateFormatter.format( - date: widget.message.timestamp, - showFullDate: true, - ), - style: widget.options.textstyles?.dateTextStyle ?? - theme.textTheme.labelSmall, - ), - ), - ], - ), - ], - Padding( - padding: const EdgeInsets.only(top: 3.0), - child: widget.message is ChatTextMessageModel - ? Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - (widget.message as ChatTextMessageModel).text, - style: widget.options.textstyles - ?.messageTextStyle ?? - theme.textTheme.bodySmall, - ), - ), - if (widget.showTime && - !isSameMinute && - !isNewDate && - !hasHeader) - Text( - dateFormatter - .format( - date: widget.message.timestamp, - showFullDate: true, - ) - .split(" ") - .last, - style: widget - .options.textstyles?.dateTextStyle ?? - theme.textTheme.labelSmall, - textAlign: TextAlign.end, - ), - ], - ) - : CachedNetworkImage( - imageUrl: (widget.message as ChatImageMessageModel) - .imageUrl, - ), - ), - ], - ), - ), - ), - ], - ), - ); - } -} diff --git a/packages/flutter_chat_view/lib/src/components/chat_image.dart b/packages/flutter_chat_view/lib/src/components/chat_image.dart deleted file mode 100644 index 1427b99..0000000 --- a/packages/flutter_chat_view/lib/src/components/chat_image.dart +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:cached_network_image/cached_network_image.dart"; -import "package:flutter/material.dart"; - -/// A stateless widget representing an image in the chat. -class ChatImage extends StatelessWidget { - /// Constructs a [ChatImage] widget. - /// - /// [image]: The URL of the image. - /// - /// [size]: The size of the image widget. - const ChatImage({ - required this.image, - this.size = 40, - super.key, - }); - - /// The URL of the image. - final String image; - - /// The size of the image widget. - final double size; - - @override - Widget build(BuildContext context) => Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(40.0), - ), - width: size, - height: size, - child: image.isNotEmpty - ? CachedNetworkImage( - fadeInDuration: Duration.zero, - imageUrl: image, - fit: BoxFit.cover, - ) - : null, - ); -} diff --git a/packages/flutter_chat_view/lib/src/components/chat_row.dart b/packages/flutter_chat_view/lib/src/components/chat_row.dart deleted file mode 100644 index 044051a..0000000 --- a/packages/flutter_chat_view/lib/src/components/chat_row.dart +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter/material.dart"; -import "package:flutter_chat_view/flutter_chat_view.dart"; - -class ChatRow extends StatelessWidget { - const ChatRow({ - required this.title, - required this.options, - this.unreadMessages = 0, - this.lastUsed, - this.subTitle, - this.avatar, - super.key, - }); - - /// The title of the chat. - final String title; - - /// The number of unread messages in the chat. - final int unreadMessages; - - /// The last time the chat was used. - final String? lastUsed; - - /// The subtitle of the chat. - final String? subTitle; - - /// The avatar associated with the chat. - final Widget? avatar; - - final ChatOptions options; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 10.0), - child: avatar, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: options.textstyles?.senderTextStyle ?? - theme.textTheme.titleMedium, - ), - if (subTitle != null) ...[ - Padding( - padding: const EdgeInsets.only(top: 3.0), - child: Text( - subTitle!, - style: unreadMessages > 0 - ? options.textstyles?.messageTextStyle!.copyWith( - fontWeight: FontWeight.w800, - ) ?? - theme.textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w800, - ) - : options.textstyles?.messageTextStyle ?? - theme.textTheme.bodySmall, - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - ), - ], - ], - ), - ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (lastUsed != null) ...[ - Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Text( - lastUsed!, - style: options.textstyles?.dateTextStyle ?? - theme.textTheme.labelSmall, - ), - ), - ], - if (unreadMessages > 0) ...[ - Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - shape: BoxShape.circle, - ), - child: Center( - child: Text( - unreadMessages.toString(), - style: const TextStyle( - fontSize: 14, - ), - ), - ), - ), - ], - ], - ), - ], - ); - } -} diff --git a/packages/flutter_chat_view/lib/src/components/image_loading_snackbar.dart b/packages/flutter_chat_view/lib/src/components/image_loading_snackbar.dart deleted file mode 100644 index ab6e23c..0000000 --- a/packages/flutter_chat_view/lib/src/components/image_loading_snackbar.dart +++ /dev/null @@ -1,19 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_chat_view/flutter_chat_view.dart"; - -SnackBar getImageLoadingSnackbar(ChatTranslations translations) => SnackBar( - duration: const Duration(minutes: 1), - content: Row( - children: [ - const SizedBox( - width: 25, - height: 25, - child: CircularProgressIndicator(color: Colors.grey), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text(translations.imageUploading), - ), - ], - ), - ); diff --git a/packages/flutter_chat_view/lib/src/components/image_picker_popup.dart b/packages/flutter_chat_view/lib/src/components/image_picker_popup.dart deleted file mode 100644 index 66f54a3..0000000 --- a/packages/flutter_chat_view/lib/src/components/image_picker_popup.dart +++ /dev/null @@ -1,33 +0,0 @@ -import "dart:typed_data"; - -import "package:flutter/material.dart"; -import "package:flutter_chat_view/flutter_chat_view.dart"; -import "package:flutter_chat_view/src/components/image_loading_snackbar.dart"; - -Future onPressSelectImage( - BuildContext context, - ChatTranslations translations, - ChatOptions options, - Function(Uint8List image) onUploadImage, -) async => - showModalBottomSheet( - context: context, - builder: (BuildContext context) => options.imagePickerContainerBuilder( - () => Navigator.of(context).pop(), - translations, - context, - ), - ).then( - (image) async { - if (image == null) return; - var messenger = ScaffoldMessenger.of(context) - ..showSnackBar( - getImageLoadingSnackbar(translations), - ) - ..activate(); - await onUploadImage(image); - Future.delayed(const Duration(seconds: 1), () { - messenger.hideCurrentSnackBar(); - }); - }, - ); diff --git a/packages/flutter_chat_view/lib/src/config/chat_options.dart b/packages/flutter_chat_view/lib/src/config/chat_options.dart deleted file mode 100644 index 4eeba87..0000000 --- a/packages/flutter_chat_view/lib/src/config/chat_options.dart +++ /dev/null @@ -1,334 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter/material.dart"; -import "package:flutter_chat_view/flutter_chat_view.dart"; -import "package:flutter_image_picker/flutter_image_picker.dart"; -import "package:flutter_profile/flutter_profile.dart"; - -class ChatOptions { - const ChatOptions({ - this.newChatButtonBuilder = _createNewChatButton, - this.messageInputBuilder = _createMessageInput, - this.chatRowContainerBuilder = _createChatRowContainer, - this.imagePickerContainerBuilder = _createImagePickerContainer, - this.chatScreenScaffoldBuilder = _createChatScreenScaffold, - this.chatDetailScaffoldBuilder = _createChatScreenScaffold, - this.chatProfileScaffoldBuilder = _createChatScreenScaffold, - this.newChatScreenScaffoldBuilder = _createChatScreenScaffold, - this.newGroupChatScreenScaffoldBuilder = _createChatScreenScaffold, - this.newGroupChatOverviewScaffoldBuilder = _createChatScreenScaffold, - this.userAvatarBuilder = _createUserAvatar, - this.groupAvatarBuilder = _createGroupAvatar, - this.noChatsPlaceholderBuilder = _createNoChatsPlaceholder, - this.noUsersPlaceholderBuilder = _createNoUsersPlaceholder, - this.paddingAroundChatList, - this.textstyles, - this.dateformat, - }); - - /// Builder function for the new chat button. - final ButtonBuilder newChatButtonBuilder; - - /// Builder function for the message input field. - final TextInputBuilder messageInputBuilder; - - /// Builder function for the container wrapping each chat row. - final ContainerBuilder chatRowContainerBuilder; - - /// Builder function for the container wrapping the image picker. - final ImagePickerContainerBuilder imagePickerContainerBuilder; - - /// Builder function for the scaffold containing the chat view. - final ScaffoldBuilder chatScreenScaffoldBuilder; - - /// Builder function for the scaffold containing the chat detail view. - final ScaffoldBuilder chatDetailScaffoldBuilder; - - /// Builder function for the scaffold containing the chat profile view. - final ScaffoldBuilder chatProfileScaffoldBuilder; - - /// Builder function for the scaffold containing the new chat view. - final ScaffoldBuilder newChatScreenScaffoldBuilder; - - /// Builder function for the scaffold containing the new groupchat view. - final ScaffoldBuilder newGroupChatScreenScaffoldBuilder; - - /// Builder function for the scaffold containing the new - /// groupchat overview view. - final ScaffoldBuilder newGroupChatOverviewScaffoldBuilder; - - /// Builder function for the user avatar. - final UserAvatarBuilder userAvatarBuilder; - - /// Builder function for the group avatar. - final GroupAvatarBuilder groupAvatarBuilder; - - /// Builder function for the placeholder shown when no chats are available. - final NoChatsPlaceholderBuilder noChatsPlaceholderBuilder; - - /// Builder function for the placeholder shown when no users are available. - final NoUsersPlaceholderBuilder noUsersPlaceholderBuilder; - - /// The padding around the chat list. - final EdgeInsets? paddingAroundChatList; - - final ChatTextStyles? textstyles; - - // ignore: avoid_positional_boolean_parameters - final String Function(bool showFullDate, DateTime date)? dateformat; -} - -Widget _createNewChatButton( - BuildContext context, - VoidCallback onPressed, - ChatTranslations translations, -) { - var theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 24, - horizontal: 4, - ), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.primary, - fixedSize: const Size(254, 44), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(56), - ), - ), - onPressed: onPressed, - child: Text( - translations.newChatButton, - style: theme.textTheme.displayLarge, - ), - ), - ); -} - -Widget _createMessageInput( - TextEditingController textEditingController, - Widget suffixIcon, - ChatTranslations translations, - BuildContext context, -) { - var theme = Theme.of(context); - return TextField( - style: theme.textTheme.bodySmall, - textCapitalization: TextCapitalization.sentences, - controller: textEditingController, - decoration: InputDecoration( - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(25), - borderSide: const BorderSide( - color: Colors.black, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(25), - borderSide: const BorderSide( - color: Colors.black, - ), - ), - contentPadding: const EdgeInsets.symmetric( - vertical: 0, - horizontal: 30, - ), - hintText: translations.messagePlaceholder, - hintStyle: theme.textTheme.bodyMedium!.copyWith( - color: theme.textTheme.bodyMedium!.color!.withOpacity(0.5), - ), - fillColor: Colors.white, - filled: true, - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(25), - ), - borderSide: BorderSide.none, - ), - suffixIcon: suffixIcon, - ), - ); -} - -Widget _createChatRowContainer( - Widget chatRow, - BuildContext context, -) { - var theme = Theme.of(context); - return DecoratedBox( - decoration: BoxDecoration( - color: Colors.transparent, - border: Border( - bottom: BorderSide( - color: theme.dividerColor, - width: 0.5, - ), - ), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: chatRow, - ), - ); -} - -Widget _createImagePickerContainer( - VoidCallback onClose, - ChatTranslations translations, - BuildContext context, -) { - var theme = Theme.of(context); - return Container( - padding: const EdgeInsets.all(8.0), - color: Colors.white, - child: ImagePicker( - imagePickerTheme: ImagePickerTheme( - title: translations.imagePickerTitle, - titleTextSize: 16, - titleAlignment: TextAlign.center, - iconSize: 60.0, - makePhotoText: translations.takePicture, - selectImageText: translations.uploadFile, - selectImageIcon: const Icon( - Icons.insert_drive_file_rounded, - size: 60, - ), - ), - customButton: TextButton( - onPressed: onClose, - child: Text( - translations.cancelImagePickerBtn, - style: theme.textTheme.bodyMedium!.copyWith( - decoration: TextDecoration.underline, - ), - ), - ), - ), - ); -} - -Scaffold _createChatScreenScaffold( - AppBar appbar, - Widget body, - Color backgroundColor, -) => - Scaffold( - appBar: appbar, - body: body, - backgroundColor: backgroundColor, - ); - -Widget _createUserAvatar( - ChatUserModel user, - double size, -) => - Avatar( - boxfit: BoxFit.cover, - user: User( - firstName: user.firstName, - lastName: user.lastName, - imageUrl: user.imageUrl != "" ? user.imageUrl : null, - ), - size: size, - ); - -Widget _createGroupAvatar( - String groupName, - String? imageUrl, - double size, -) => - Avatar( - boxfit: BoxFit.cover, - user: User( - firstName: groupName, - lastName: null, - imageUrl: imageUrl != "" ? imageUrl : null, - ), - size: size, - ); - -Widget _createNoChatsPlaceholder( - ChatTranslations translations, - BuildContext context, -) { - var theme = Theme.of(context); - return Center( - child: Text( - translations.noChatsFound, - textAlign: TextAlign.center, - style: theme.textTheme.bodySmall, - ), - ); -} - -Widget _createNoUsersPlaceholder( - ChatTranslations translations, - BuildContext context, -) { - var theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Align( - alignment: Alignment.topCenter, - child: Text( - translations.noUsersFound, - textAlign: TextAlign.center, - style: theme.textTheme.bodySmall, - ), - ), - ); -} - -typedef ButtonBuilder = Widget Function( - BuildContext context, - VoidCallback onPressed, - ChatTranslations translations, -); - -typedef TextInputBuilder = Widget Function( - TextEditingController textEditingController, - Widget suffixIcon, - ChatTranslations translations, - BuildContext context, -); - -typedef ContainerBuilder = Widget Function( - Widget child, - BuildContext context, -); - -typedef ImagePickerContainerBuilder = Widget Function( - VoidCallback onClose, - ChatTranslations translations, - BuildContext context, -); - -typedef ScaffoldBuilder = Scaffold Function( - AppBar appBar, - Widget body, - Color backgroundColor, -); - -typedef UserAvatarBuilder = Widget Function( - ChatUserModel user, - double size, -); - -typedef GroupAvatarBuilder = Widget Function( - String groupName, - String? imageUrl, - double size, -); - -typedef NoChatsPlaceholderBuilder = Widget Function( - ChatTranslations translations, - BuildContext context, -); - -typedef NoUsersPlaceholderBuilder = Widget Function( - ChatTranslations translations, - BuildContext context, -); diff --git a/packages/flutter_chat_view/lib/src/config/chat_text_styles.dart b/packages/flutter_chat_view/lib/src/config/chat_text_styles.dart deleted file mode 100644 index 6ec6dd7..0000000 --- a/packages/flutter_chat_view/lib/src/config/chat_text_styles.dart +++ /dev/null @@ -1,13 +0,0 @@ -import "package:flutter/material.dart"; - -class ChatTextStyles { - ChatTextStyles({ - this.senderTextStyle, - this.messageTextStyle, - this.dateTextStyle, - }); - - final TextStyle? senderTextStyle; - final TextStyle? messageTextStyle; - final TextStyle? dateTextStyle; -} diff --git a/packages/flutter_chat_view/lib/src/screens/chat_detail_screen.dart b/packages/flutter_chat_view/lib/src/screens/chat_detail_screen.dart deleted file mode 100644 index 7b7faec..0000000 --- a/packages/flutter_chat_view/lib/src/screens/chat_detail_screen.dart +++ /dev/null @@ -1,269 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "dart:async"; -import "dart:typed_data"; - -import "package:flutter/material.dart"; -import "package:flutter_chat_view/flutter_chat_view.dart"; -import "package:flutter_chat_view/src/components/chat_bottom.dart"; -import "package:flutter_chat_view/src/components/chat_detail_row.dart"; -import "package:flutter_chat_view/src/components/image_picker_popup.dart"; - -class ChatDetailScreen extends StatefulWidget { - const ChatDetailScreen({ - required this.options, - required this.onMessageSubmit, - required this.onUploadImage, - required this.onReadChat, - required this.service, - required this.pageSize, - required this.chatId, - required this.textfieldBottomPadding, - required this.onPressChatTitle, - required this.onPressUserProfile, - this.chatTitleBuilder, - this.usernameBuilder, - this.loadingWidgetBuilder, - this.translations = const ChatTranslations.empty(), - this.iconColor, - this.iconDisabledColor, - this.showTime = false, - super.key, - }); - - final String chatId; - - /// The id of the current user that is viewing the chat. - - final ChatOptions options; - final ChatTranslations translations; - final Future Function(Uint8List image) onUploadImage; - final Future Function(String text) onMessageSubmit; - // called at the start of the screen to set the chat to read - // or when a new message is received - final Future Function(ChatModel chat) onReadChat; - final Function(BuildContext context, ChatModel chat) onPressChatTitle; - - /// The color of the icon buttons in the chat bottom. - final Color? iconColor; - final bool showTime; - final ChatService service; - final int pageSize; - final double textfieldBottomPadding; - final Color? iconDisabledColor; - final Function(ChatUserModel user) onPressUserProfile; - // ignore: avoid_positional_boolean_parameters - final Widget? Function(BuildContext context)? loadingWidgetBuilder; - final Widget Function(String userFullName)? usernameBuilder; - final Widget Function(String chatTitle)? chatTitleBuilder; - - @override - State createState() => _ChatDetailScreenState(); -} - -class _ChatDetailScreenState extends State { - // stream listener that needs to be disposed later - ChatUserModel? currentUser; - ScrollController controller = ScrollController(); - bool showIndicator = false; - late ChatDetailService messageSubscription; - Stream>? stream; - ChatMessageModel? previousMessage; - List detailRows = []; - ChatModel? chat; - - @override - void initState() { - super.initState(); - messageSubscription = widget.service.chatDetailService; - messageSubscription.addListener(onListen); - Future.delayed(Duration.zero, () async { - chat = - await widget.service.chatOverviewService.getChatById(widget.chatId); - - if (detailRows.isEmpty && context.mounted) { - await widget.service.chatDetailService.fetchMoreMessage( - widget.pageSize, - chat!.id!, - ); - } - stream = widget.service.chatDetailService.getMessagesStream(chat!.id!); - stream?.listen((event) {}); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - await widget.onReadChat(chat!); - }); - }); - } - - void onListen() { - var chatMessages = []; - chatMessages = widget.service.chatDetailService.getMessages(); - detailRows = []; - previousMessage = null; - for (var message in chatMessages) { - detailRows.add( - ChatDetailRow( - options: widget.options, - showTime: true, - message: message, - translations: widget.translations, - userAvatarBuilder: widget.options.userAvatarBuilder, - previousMessage: previousMessage, - onPressUserProfile: widget.onPressUserProfile, - usernameBuilder: widget.usernameBuilder, - ), - ); - previousMessage = message; - } - detailRows = detailRows.reversed.toList(); - - if (mounted) { - WidgetsBinding.instance.addPostFrameCallback((_) async { - await widget.onReadChat(chat!); - }); - - setState(() {}); - } - } - - @override - void dispose() { - messageSubscription.removeListener(onListen); - widget.service.chatDetailService.stopListeningForMessages(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - return FutureBuilder( - // ignore: discarded_futures - future: widget.service.chatOverviewService.getChatById(widget.chatId), - builder: (context, AsyncSnapshot snapshot) { - var chatModel = snapshot.data; - var chatTitle = (chatModel is GroupChatModel) - ? chatModel.title - : (chatModel is PersonalChatModel) - ? chatModel.user.firstName ?? widget.translations.anonymousUser - : ""; - return widget.options.chatDetailScaffoldBuilder( - AppBar( - iconTheme: theme.appBarTheme.iconTheme ?? - const IconThemeData(color: Colors.white), - centerTitle: true, - leading: GestureDetector( - onTap: () { - Navigator.popUntil(context, (route) => route.isFirst); - }, - child: const Icon( - Icons.arrow_back_ios, - ), - ), - title: GestureDetector( - onTap: () => widget.onPressChatTitle.call(context, chatModel!), - child: widget.chatTitleBuilder?.call(chatTitle) ?? - Text( - chatTitle, - overflow: TextOverflow.ellipsis, - ), - ), - ), - Stack( - children: [ - Column( - children: [ - Expanded( - child: Listener( - onPointerMove: (event) async { - if (!showIndicator && - controller.offset >= - controller.position.maxScrollExtent && - !controller.position.outOfRange) { - setState(() { - showIndicator = true; - }); - await widget.service.chatDetailService - .fetchMoreMessage( - widget.pageSize, - widget.chatId, - ); - Future.delayed(const Duration(seconds: 2), () { - if (mounted) { - setState(() { - showIndicator = false; - }); - } - }); - } - }, - child: ListView( - shrinkWrap: true, - physics: const AlwaysScrollableScrollPhysics(), - controller: controller, - reverse: detailRows.isNotEmpty, - padding: const EdgeInsets.only(top: 24.0), - children: [ - if (detailRows.isEmpty && !showIndicator) ...[ - Center( - child: Text( - (chatModel is GroupChatModel) - ? widget.translations - .writeFirstMessageInGroupChat - : widget - .translations.writeMessageToStartChat, - style: theme.textTheme.bodySmall, - ), - ), - ], - ...detailRows, - ], - ), - ), - ), - if (chatModel != null) ...[ - ChatBottom( - chat: chatModel, - messageInputBuilder: widget.options.messageInputBuilder, - onPressSelectImage: () async => onPressSelectImage.call( - context, - widget.translations, - widget.options, - widget.onUploadImage, - ), - onMessageSubmit: widget.onMessageSubmit, - translations: widget.translations, - iconColor: widget.iconColor, - iconDisabledColor: widget.iconDisabledColor, - ), - ], - SizedBox( - height: widget.textfieldBottomPadding, - ), - ], - ), - if (showIndicator) ...[ - widget.loadingWidgetBuilder?.call(context) ?? - const Column( - children: [ - SizedBox( - height: 10, - ), - Center(child: CircularProgressIndicator()), - SizedBox( - height: 10, - ), - ], - ), - ], - ], - ), - theme.scaffoldBackgroundColor, - ); - }, - ); - } -} diff --git a/packages/flutter_chat_view/lib/src/screens/chat_profile_screen.dart b/packages/flutter_chat_view/lib/src/screens/chat_profile_screen.dart deleted file mode 100644 index b16ff06..0000000 --- a/packages/flutter_chat_view/lib/src/screens/chat_profile_screen.dart +++ /dev/null @@ -1,219 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_chat_view/flutter_chat_view.dart"; -import "package:flutter_profile/flutter_profile.dart"; - -class ChatProfileScreen extends StatefulWidget { - const ChatProfileScreen({ - required this.chatService, - required this.chatId, - required this.translations, - required this.onTapUser, - required this.options, - required this.onPressStartChat, - required this.currentUserId, - this.userId, - super.key, - }); - - /// Translations for the chat. - final ChatTranslations translations; - - /// Chat service instance. - final ChatService chatService; - - /// ID of the chat. - final String chatId; - - /// ID of the user (optional). - final String? userId; - - /// Callback function for tapping on a user. - final Function(ChatUserModel user) onTapUser; - - /// Chat options. - final ChatOptions options; - - /// Callback function for starting a chat. - final Function(ChatUserModel user) onPressStartChat; - - /// The current user. - final String currentUserId; - - @override - State createState() => _ProfileScreenState(); -} - -class _ProfileScreenState extends State { - @override - Widget build(BuildContext context) { - var hasUser = widget.userId == null; - var theme = Theme.of(context); - return FutureBuilder( - future: hasUser - // ignore: discarded_futures - ? widget.chatService.chatOverviewService.getChatById(widget.chatId) - // ignore: discarded_futures - : widget.chatService.chatUserService.getUser(widget.userId!), - builder: (context, snapshot) { - var data = snapshot.data; - User? user; - - if (data is ChatUserModel) { - user = User( - firstName: data.firstName, - lastName: data.lastName, - imageUrl: data.imageUrl, - ); - } - if (data is PersonalChatModel) { - user = User( - firstName: data.user.firstName, - lastName: data.user.lastName, - imageUrl: data.user.imageUrl, - ); - } else if (data is GroupChatModel) { - user = User( - firstName: data.title, - imageUrl: data.imageUrl, - ); - } - return widget.options.chatProfileScaffoldBuilder( - AppBar( - iconTheme: theme.appBarTheme.iconTheme ?? - const IconThemeData(color: Colors.white), - title: Text( - (data is ChatUserModel) - ? '${data.firstName ?? ''} ${data.lastName ?? ''}' - : (data is PersonalChatModel) - ? data.user.fullName ?? "" - : (data is GroupChatModel) - ? data.title - : "", - ), - ), - snapshot.hasData - ? Stack( - children: [ - ListView( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Column( - children: [ - widget.options.userAvatarBuilder( - ChatUserModel( - firstName: user!.firstName, - lastName: user.lastName, - imageUrl: user.imageUrl, - ), - 60, - ), - ], - ), - ), - const Divider( - color: Colors.white, - thickness: 10, - ), - if (data is GroupChatModel) ...[ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 24, - horizontal: 20, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.translations.groupProfileBioHeader, - style: theme.textTheme.titleMedium, - ), - const SizedBox( - height: 12, - ), - Text( - data.bio ?? "", - style: theme.textTheme.bodyMedium! - .copyWith(color: Colors.black), - ), - const SizedBox( - height: 12, - ), - Text( - widget.translations.chatProfileUsers, - style: theme.textTheme.titleMedium, - ), - const SizedBox( - height: 12, - ), - Wrap( - children: [ - ...data.users.map( - (user) => Padding( - padding: const EdgeInsets.only( - bottom: 8, - right: 8, - ), - child: GestureDetector( - onTap: () { - widget.onTapUser.call(user); - }, - child: Column( - mainAxisAlignment: - MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - widget.options.userAvatarBuilder( - user, - 44, - ), - ], - ), - ), - ), - ), - ], - ), - ], - ), - ), - ], - ], - ), - if (data is ChatUserModel && - widget.currentUserId != data.id) ...[ - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 24, - horizontal: 80, - ), - child: FilledButton( - onPressed: () { - widget.onPressStartChat(data); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.translations.newChatButton, - style: theme.textTheme.displayLarge, - ), - ], - ), - ), - ), - ), - ], - ], - ) - : const Center( - child: CircularProgressIndicator(), - ), - theme.scaffoldBackgroundColor, - ); - }, - ); - } -} diff --git a/packages/flutter_chat_view/lib/src/screens/chat_screen.dart b/packages/flutter_chat_view/lib/src/screens/chat_screen.dart deleted file mode 100644 index d1a387c..0000000 --- a/packages/flutter_chat_view/lib/src/screens/chat_screen.dart +++ /dev/null @@ -1,378 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -// ignore_for_file: lines_longer_than_80_chars - -import "dart:async"; - -import "package:flutter/material.dart"; -import "package:flutter_chat_view/flutter_chat_view.dart"; -import "package:flutter_chat_view/src/services/date_formatter.dart"; - -class ChatScreen extends StatefulWidget { - const ChatScreen({ - required this.options, - required this.onPressStartChat, - required this.onPressChat, - required this.onDeleteChat, - required this.service, - this.unreadMessageTextStyle, - this.onNoChats, - this.deleteChatDialog, - this.translations = const ChatTranslations.empty(), - this.disableDismissForPermanentChats = false, - super.key, - }); - - /// Chat options. - final ChatOptions options; - - /// Chat service instance. - final ChatService service; - - /// Callback function for starting a chat. - final Function()? onPressStartChat; - - /// 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; - - /// Callback function for handling when there are no chats. - final Function()? onNoChats; - - /// Method to optionally change the bottom sheet dialog. - final Future Function(BuildContext, ChatModel)? deleteChatDialog; - - /// Translations for the chat. - final ChatTranslations translations; - - /// Disables the swipe to dismiss feature for chats that are not deletable. - final bool disableDismissForPermanentChats; - final TextStyle? unreadMessageTextStyle; - - @override - State createState() => _ChatScreenState(); -} - -class _ChatScreenState extends State { - bool _hasCalledOnNoChats = false; - ScrollController controller = ScrollController(); - bool showIndicator = false; - Stream>? chats; - List deletedChats = []; - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - var dateFormatter = DateFormatter(options: widget.options); - - var translations = widget.translations; - var theme = Theme.of(context); - return widget.options.chatScreenScaffoldBuilder( - AppBar( - title: Text( - translations.chatsTitle, - ), - centerTitle: true, - actions: [ - StreamBuilder( - stream: - widget.service.chatOverviewService.getUnreadChatsCountStream(), - builder: (BuildContext context, snapshot) => Align( - alignment: Alignment.centerRight, - child: Visibility( - visible: (snapshot.data ?? 0) > 0, - child: Padding( - padding: const EdgeInsets.only(right: 22.0), - child: Text( - "${snapshot.data ?? 0} ${translations.chatsUnread}", - style: widget.unreadMessageTextStyle ?? - theme.textTheme.bodySmall!.copyWith( - color: Colors.white, - ), - ), - ), - ), - ), - ), - ], - ), - Column( - children: [ - Expanded( - child: ListView( - controller: controller, - physics: const AlwaysScrollableScrollPhysics(), - padding: widget.options.paddingAroundChatList ?? - const EdgeInsets.symmetric(vertical: 16, horizontal: 28), - children: [ - StreamBuilder>( - stream: widget.service.chatOverviewService.getChatsStream(), - 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)) { - if (widget.onNoChats != null && !_hasCalledOnNoChats) { - _hasCalledOnNoChats = true; // Set the flag to true - WidgetsBinding.instance.addPostFrameCallback((_) async { - await widget.onNoChats!.call(); - }); - } - return Center( - child: Text( - translations.noChatsFound, - style: theme.textTheme.bodySmall, - ), - ); - } else { - _hasCalledOnNoChats = false; - } - return Column( - children: [ - for (ChatModel chat in (snapshot.data ?? []).where( - (chat) => !deletedChats.contains(chat.id), - )) ...[ - DecoratedBox( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: theme.dividerColor, - width: 0.5, - ), - ), - ), - child: Builder( - builder: (context) => - !(widget.disableDismissForPermanentChats && - !chat.canBeDeleted) - ? Dismissible( - confirmDismiss: (_) async => - widget.deleteChatDialog - ?.call(context, chat) ?? - _deleteDialog( - chat, - translations, - context, - ), - onDismissed: (_) { - setState(() { - deletedChats.add(chat.id!); - }); - widget.onDeleteChat(chat); - }, - secondaryBackground: const ColoredBox( - color: Colors.red, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.all(8.0), - child: Icon( - Icons.delete, - color: Colors.white, - ), - ), - ), - ), - background: const ColoredBox( - color: Colors.red, - child: Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.all(8.0), - child: Icon( - Icons.delete, - color: Colors.white, - ), - ), - ), - ), - key: ValueKey( - chat.id.toString(), - ), - child: ChatListItem( - widget: widget, - chat: chat, - translations: translations, - dateFormatter: dateFormatter, - ), - ) - : ChatListItem( - widget: widget, - chat: chat, - translations: translations, - dateFormatter: dateFormatter, - ), - ), - ), - ], - ], - ); - }, - ), - ], - ), - ), - if (widget.onPressStartChat != null) - widget.options.newChatButtonBuilder( - context, - () async { - await widget.onPressStartChat!.call(); - }, - translations, - ), - ], - ), - theme.scaffoldBackgroundColor, - ); - } - - Future _deleteDialog( - ChatModel chat, - ChatTranslations translations, - BuildContext context, - ) async { - var theme = Theme.of(context); - var title = chat.canBeDeleted - ? translations.deleteChatModalTitle - : translations.chatCantBeDeleted; - return showModalBottomSheet( - context: context, - builder: (BuildContext context) => Container( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - title, - textAlign: TextAlign.center, - style: theme.textTheme.titleMedium, - ), - const SizedBox( - height: 20, - ), - if (chat.canBeDeleted) ...[ - Text( - translations.deleteChatModalDescription, - textAlign: TextAlign.center, - style: theme.textTheme.bodyMedium, - ), - const SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 60), - child: FilledButton( - onPressed: () { - Navigator.of( - context, - ).pop(true); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.translations.deleteChatModalConfirm, - style: theme.textTheme.displayLarge, - ), - ], - ), - ), - ), - ], - TextButton( - onPressed: () { - Navigator.of( - context, - ).pop(false); - }, - child: Text( - widget.translations.deleteChatModalCancel, - style: theme.textTheme.bodyMedium!.copyWith( - color: theme.textTheme.bodyMedium!.color?.withOpacity(0.5), - decoration: TextDecoration.underline, - ), - ), - ), - ], - ), - ), - ); - } -} - -class ChatListItem extends StatelessWidget { - const ChatListItem({ - required this.widget, - required this.chat, - required this.translations, - required DateFormatter dateFormatter, - super.key, - }) : _dateFormatter = dateFormatter; - - final ChatScreen widget; - final ChatModel chat; - final ChatTranslations translations; - final DateFormatter _dateFormatter; - - @override - Widget build(BuildContext context) => GestureDetector( - onTap: () { - widget.onPressChat(chat); - }, - child: widget.options.chatRowContainerBuilder( - (chat is PersonalChatModel) - ? ChatRow( - options: widget.options, - unreadMessages: chat.unreadMessages ?? 0, - avatar: widget.options.userAvatarBuilder( - (chat as PersonalChatModel).user, - 40.0, - ), - title: (chat as PersonalChatModel).user.fullName ?? - translations.anonymousUser, - subTitle: chat.lastMessage != null - ? chat.lastMessage is ChatTextMessageModel - ? (chat.lastMessage! as ChatTextMessageModel).text - : "📷 " - "${translations.image}" - : "", - lastUsed: chat.lastUsed != null - ? _dateFormatter.format( - date: chat.lastUsed!, - ) - : null, - ) - : ChatRow( - options: widget.options, - title: (chat as GroupChatModel).title, - unreadMessages: chat.unreadMessages ?? 0, - subTitle: chat.lastMessage != null - ? chat.lastMessage is ChatTextMessageModel - ? (chat.lastMessage! as ChatTextMessageModel).text - : "📷 " - "${translations.image}" - : "", - avatar: widget.options.groupAvatarBuilder( - (chat as GroupChatModel).title, - (chat as GroupChatModel).imageUrl, - 40.0, - ), - lastUsed: chat.lastUsed != null - ? _dateFormatter.format( - date: chat.lastUsed!, - ) - : null, - ), - context, - ), - ); -} diff --git a/packages/flutter_chat_view/lib/src/screens/new_chat_screen.dart b/packages/flutter_chat_view/lib/src/screens/new_chat_screen.dart deleted file mode 100644 index 0e7903e..0000000 --- a/packages/flutter_chat_view/lib/src/screens/new_chat_screen.dart +++ /dev/null @@ -1,228 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter/material.dart"; -import "package:flutter_chat_view/flutter_chat_view.dart"; - -class NewChatScreen extends StatefulWidget { - const NewChatScreen({ - required this.options, - required this.onPressCreateChat, - required this.service, - required this.onPressCreateGroupChat, - this.showGroupChatButton = true, - this.translations = const ChatTranslations.empty(), - super.key, - }); - - /// Chat options. - final ChatOptions options; - - /// Chat service instance. - final ChatService service; - - /// Callback function for creating a new chat with a user. - final Function(ChatUserModel) onPressCreateChat; - - /// Callback function for creating a new group chat. - final Function() onPressCreateGroupChat; - - /// Option to enable the group chat creation button. - final bool showGroupChatButton; - - /// Translations for the chat. - final ChatTranslations translations; - - @override - State createState() => _NewChatScreenState(); -} - -class _NewChatScreenState extends State { - final FocusNode _textFieldFocusNode = FocusNode(); - bool _isSearching = false; - String query = ""; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return widget.options.newChatScreenScaffoldBuilder( - AppBar( - iconTheme: theme.appBarTheme.iconTheme ?? - const IconThemeData(color: Colors.white), - title: _buildSearchField(), - actions: [ - _buildSearchIcon(), - ], - ), - Column( - children: [ - if (widget.showGroupChatButton && !_isSearching) ...[ - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - top: 20, - ), - child: FilledButton( - onPressed: () async { - await widget.onPressCreateGroupChat(); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.groups, - ), - const SizedBox( - width: 4, - ), - Text( - widget.translations.newGroupChatButton, - style: theme.textTheme.displayLarge, - ), - ], - ), - ), - ), - ], - Expanded( - child: FutureBuilder>( - // ignore: discarded_futures - future: widget.service.chatUserService.getAllUsers(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Text("Error: ${snapshot.error}"); - } else if (snapshot.hasData) { - return _buildUserList(snapshot.data!); - } else { - return widget.options - .noUsersPlaceholderBuilder(widget.translations, context); - } - }, - ), - ), - ], - ), - theme.scaffoldBackgroundColor, - ); - } - - Widget _buildSearchField() { - var theme = Theme.of(context); - - return _isSearching - ? TextField( - focusNode: _textFieldFocusNode, - onChanged: (value) { - setState(() { - query = value; - }); - }, - decoration: InputDecoration( - hintText: widget.translations.searchPlaceholder, - hintStyle: - theme.textTheme.bodyMedium!.copyWith(color: Colors.white), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: theme.colorScheme.primary, - ), - ), - ), - style: theme.textTheme.bodySmall!.copyWith(color: Colors.white), - cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white, - ) - : Text( - widget.translations.newChatTitle, - ); - } - - Widget _buildSearchIcon() { - var theme = Theme.of(context); - - return IconButton( - onPressed: () { - setState(() { - _isSearching = !_isSearching; - query = ""; - }); - - if (_isSearching) { - _textFieldFocusNode.requestFocus(); - } - }, - icon: Icon( - _isSearching ? Icons.close : Icons.search, - color: theme.appBarTheme.iconTheme?.color ?? Colors.white, - ), - ); - } - - Widget _buildUserList(List users) { - var theme = Theme.of(context); - var filteredUsers = users - .where( - (user) => - user.fullName?.toLowerCase().contains( - query.toLowerCase(), - ) ?? - false, - ) - .toList(); - - if (_textFieldFocusNode.hasFocus && query.isEmpty) { - return Padding( - padding: const EdgeInsets.only(top: 20.0), - child: Align( - alignment: Alignment.topCenter, - child: Text( - widget.translations.startTyping, - style: theme.textTheme.bodySmall, - ), - ), - ); - } - - if (filteredUsers.isEmpty) { - return widget.options - .noUsersPlaceholderBuilder(widget.translations, context); - } - var isPressed = false; - return Padding( - padding: widget.options.paddingAroundChatList ?? - const EdgeInsets.symmetric(vertical: 8, horizontal: 12), - child: ListView.builder( - itemCount: filteredUsers.length, - itemBuilder: (context, index) { - var user = filteredUsers[index]; - return InkWell( - onTap: () async { - if (!isPressed) { - isPressed = true; - await widget.onPressCreateChat(user); - isPressed = false; - } - }, - child: widget.options.chatRowContainerBuilder( - Row( - children: [ - widget.options.userAvatarBuilder(user, 44), - const SizedBox( - width: 12, - ), - Text( - user.fullName ?? widget.translations.anonymousUser, - style: theme.textTheme.titleMedium, - ), - ], - ), - context, - ), - ); - }, - ), - ); - } -} diff --git a/packages/flutter_chat_view/lib/src/screens/new_group_chat_overview_screen.dart b/packages/flutter_chat_view/lib/src/screens/new_group_chat_overview_screen.dart deleted file mode 100644 index 168ce86..0000000 --- a/packages/flutter_chat_view/lib/src/screens/new_group_chat_overview_screen.dart +++ /dev/null @@ -1,319 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "dart:typed_data"; - -import "package:flutter/material.dart"; -import "package:flutter_chat_view/flutter_chat_view.dart"; -import "package:flutter_chat_view/src/components/image_picker_popup.dart"; - -class NewGroupChatOverviewScreen extends StatefulWidget { - const NewGroupChatOverviewScreen({ - required this.options, - required this.onPressCompleteGroupChatCreation, - required this.service, - this.translations = const ChatTranslations.empty(), - super.key, - }); - - final ChatOptions options; - final ChatTranslations translations; - final ChatService service; - final Function( - List users, - String groupchatName, - String? groupchatBio, - Uint8List? imageBytes, - ) onPressCompleteGroupChatCreation; - - @override - State createState() => - _NewGroupChatOverviewScreenState(); -} - -class _NewGroupChatOverviewScreenState - extends State { - final TextEditingController _chatNameController = TextEditingController(); - final TextEditingController _bioController = TextEditingController(); - Uint8List? image; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - var formKey = GlobalKey(); - var isPressed = false; - var users = widget.service.chatOverviewService.currentlySelectedUsers; - - void onUploadImage(groupImage) { - setState(() { - image = groupImage; - }); - } - - return widget.options.newGroupChatOverviewScaffoldBuilder( - AppBar( - iconTheme: theme.appBarTheme.iconTheme ?? - const IconThemeData(color: Colors.white), - backgroundColor: theme.appBarTheme.backgroundColor, - title: Text( - widget.translations.newGroupChatTitle, - ), - ), - Stack( - children: [ - SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 40, - ), - Center( - child: Stack( - children: [ - GestureDetector( - onTap: () async { - await onPressSelectImage( - context, - widget.translations, - widget.options, - onUploadImage, - ); - }, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: const Color(0xFFD9D9D9), - borderRadius: BorderRadius.circular(40), - image: image != null - ? DecorationImage( - image: MemoryImage(image!), - fit: BoxFit.cover, - ) - : null, - ), - child: image == null - ? const Icon(Icons.image) - : null, - ), - ), - if (image != null) - Positioned.directional( - textDirection: Directionality.of(context), - end: 0, - child: Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: const Color(0xFFBCBCBC), - borderRadius: BorderRadius.circular(40), - ), - child: Center( - child: GestureDetector( - onTap: () { - setState(() { - image = null; - }); - }, - child: const Icon( - Icons.close, - size: 12, - ), - ), - ), - ), - ) - else - const SizedBox.shrink(), - ], - ), - ), - const SizedBox( - height: 40, - ), - Text( - widget.translations.groupChatNameFieldHeader, - style: theme.textTheme.titleMedium, - ), - const SizedBox( - height: 12, - ), - TextFormField( - style: theme.textTheme.bodySmall, - controller: _chatNameController, - decoration: InputDecoration( - fillColor: Colors.white, - filled: true, - hintText: widget.translations.groupNameHintText, - hintStyle: theme.textTheme.bodyMedium!.copyWith( - color: theme.textTheme.bodyMedium!.color! - .withOpacity(0.5), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return widget.translations.groupNameValidatorEmpty; - } - if (value.length > 15) - return widget.translations.groupNameValidatorTooLong; - return null; - }, - ), - const SizedBox( - height: 16, - ), - Text( - widget.translations.groupBioFieldHeader, - style: theme.textTheme.titleMedium, - ), - const SizedBox( - height: 12, - ), - TextFormField( - style: theme.textTheme.bodySmall, - controller: _bioController, - minLines: null, - maxLines: 5, - decoration: InputDecoration( - fillColor: Colors.white, - filled: true, - hintText: widget.translations.groupBioHintText, - hintStyle: theme.textTheme.bodyMedium!.copyWith( - color: theme.textTheme.bodyMedium!.color! - .withOpacity(0.5), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return widget.translations.groupBioValidatorEmpty; - } - - return null; - }, - ), - const SizedBox( - height: 16, - ), - Text( - "${widget.translations.selectedMembersHeader}" - "${users.length}", - style: theme.textTheme.titleMedium, - ), - const SizedBox( - height: 12, - ), - Wrap( - children: [ - ...users.map( - _selectedUser, - ), - ], - ), - const SizedBox( - height: 80, - ), - ], - ), - ), - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 24, - horizontal: 80, - ), - child: FilledButton( - onPressed: users.isNotEmpty - ? () async { - if (!isPressed) { - isPressed = true; - if (formKey.currentState!.validate()) { - await widget.onPressCompleteGroupChatCreation( - users, - _chatNameController.text, - _bioController.text, - image, - ); - } - isPressed = false; - } - } - : null, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.translations.createGroupChatButton, - style: theme.textTheme.displayLarge, - ), - ], - ), - ), - ), - ), - ], - ), - theme.scaffoldBackgroundColor, - ); - } - - Widget _selectedUser(ChatUserModel user) => GestureDetector( - onTap: () { - setState(() { - widget.service.chatOverviewService - .removeCurrentlySelectedUser(user); - }); - }, - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: widget.options.userAvatarBuilder( - user, - 40, - ), - ), - Positioned.directional( - textDirection: Directionality.of(context), - end: 0, - child: const Icon( - Icons.cancel, - size: 20, - ), - ), - ], - ), - ); -} diff --git a/packages/flutter_chat_view/lib/src/screens/new_group_chat_screen.dart b/packages/flutter_chat_view/lib/src/screens/new_group_chat_screen.dart deleted file mode 100644 index b8b7be2..0000000 --- a/packages/flutter_chat_view/lib/src/screens/new_group_chat_screen.dart +++ /dev/null @@ -1,300 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter/material.dart"; -import "package:flutter_chat_view/flutter_chat_view.dart"; - -class NewGroupChatScreen extends StatefulWidget { - const NewGroupChatScreen({ - required this.options, - required this.onPressGroupChatOverview, - required this.service, - this.translations = const ChatTranslations.empty(), - super.key, - }); - - final ChatOptions options; - final ChatTranslations translations; - final ChatService service; - final Function(List) onPressGroupChatOverview; - - @override - State createState() => _NewGroupChatScreenState(); -} - -class _NewGroupChatScreenState extends State { - final FocusNode _textFieldFocusNode = FocusNode(); - - bool _isSearching = false; - String query = ""; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return widget.options.newGroupChatScreenScaffoldBuilder( - AppBar( - iconTheme: theme.appBarTheme.iconTheme ?? - const IconThemeData(color: Colors.white), - backgroundColor: theme.appBarTheme.backgroundColor, - title: _buildSearchField(), - actions: [ - _buildSearchIcon(), - ], - ), - FutureBuilder>( - // ignore: discarded_futures - future: widget.service.chatUserService.getAllUsers(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Text("Error: ${snapshot.error}"); - } else if (snapshot.hasData) { - return Stack( - children: [ - _buildUserList(snapshot.data!), - NextButton( - service: widget.service, - onPressGroupChatOverview: widget.onPressGroupChatOverview, - ), - ], - ); - } - return const SizedBox.shrink(); - }, - ), - theme.scaffoldBackgroundColor, - ); - } - - Widget _buildSearchField() { - var theme = Theme.of(context); - - return _isSearching - ? TextField( - focusNode: _textFieldFocusNode, - onChanged: (value) { - setState(() { - query = value; - }); - }, - decoration: InputDecoration( - hintText: widget.translations.searchPlaceholder, - hintStyle: - theme.textTheme.bodyMedium!.copyWith(color: Colors.white), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: theme.colorScheme.primary, - ), - ), - ), - style: theme.textTheme.bodySmall!.copyWith(color: Colors.white), - cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white, - ) - : Text( - widget.translations.newGroupChatButton, - ); - } - - Widget _buildSearchIcon() { - var theme = Theme.of(context); - - return IconButton( - onPressed: () { - setState(() { - _isSearching = !_isSearching; - query = ""; - }); - - if (_isSearching) { - _textFieldFocusNode.requestFocus(); - } - }, - icon: Icon( - _isSearching ? Icons.close : Icons.search, - color: theme.appBarTheme.iconTheme?.color ?? Colors.white, - ), - ); - } - - Widget _buildUserList(List users) { - var filteredUsers = users - .where( - (user) => - user.fullName?.toLowerCase().contains( - query.toLowerCase(), - ) ?? - false, - ) - .toList(); - - if (filteredUsers.isEmpty) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - widget.options - .noUsersPlaceholderBuilder(widget.translations, context), - ], - ); - } - - return UserList( - filteredUsers: filteredUsers, - options: widget.options, - translations: widget.translations, - service: widget.service, - ); - } -} - -class NextButton extends StatefulWidget { - const NextButton({ - required this.service, - required this.onPressGroupChatOverview, - super.key, - }); - - final ChatService service; - final Function(List) onPressGroupChatOverview; - - @override - State createState() => _NextButtonState(); -} - -class _NextButtonState extends State { - @override - void initState() { - widget.service.chatOverviewService.addListener(_listen); - super.initState(); - } - - @override - void dispose() { - widget.service.chatOverviewService.removeListener(_listen); - super.dispose(); - } - - void _listen() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 24, - horizontal: 80, - ), - child: FilledButton( - onPressed: widget - .service.chatOverviewService.currentlySelectedUsers.isNotEmpty - ? () async { - await widget.onPressGroupChatOverview( - widget.service.chatOverviewService.currentlySelectedUsers, - ); - } - : null, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Next", - style: theme.textTheme.displayLarge, - ), - ], - ), - ), - ), - ); - } -} - -class UserList extends StatefulWidget { - const UserList({ - required this.filteredUsers, - required this.options, - required this.translations, - required this.service, - super.key, - }); - - final List filteredUsers; - final ChatOptions options; - final ChatTranslations translations; - final ChatService service; - - @override - State createState() => _UserListState(); -} - -class _UserListState extends State { - @override - void initState() { - widget.service.chatOverviewService.addListener(_listen); - super.initState(); - } - - @override - void dispose() { - widget.service.chatOverviewService.removeListener(_listen); - super.dispose(); - } - - void _listen() { - setState(() {}); - } - - @override - Widget build(BuildContext context) => Padding( - padding: widget.options.paddingAroundChatList ?? - const EdgeInsets.symmetric(vertical: 8, horizontal: 12), - child: ListView.builder( - itemCount: widget.filteredUsers.length, - itemBuilder: (context, index) { - var user = widget.filteredUsers[index]; - var isSelected = widget - .service.chatOverviewService.currentlySelectedUsers - .any((selectedUser) => selectedUser == user); - var theme = Theme.of(context); - return widget.options.chatRowContainerBuilder( - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - widget.options.userAvatarBuilder(user, 44), - const SizedBox( - width: 12, - ), - Text( - user.fullName ?? widget.translations.anonymousUser, - style: theme.textTheme.titleMedium, - ), - ], - ), - Checkbox( - value: isSelected, - onChanged: (value) { - setState(() { - if (widget - .service.chatOverviewService.currentlySelectedUsers - .contains(user)) { - widget.service.chatOverviewService - .removeCurrentlySelectedUser(user); - } else { - widget.service.chatOverviewService - .addCurrentlySelectedUser(user); - } - }); - }, - ), - ], - ), - context, - ); - }, - ), - ); -} diff --git a/packages/flutter_chat_view/lib/src/services/profile_service.dart b/packages/flutter_chat_view/lib/src/services/profile_service.dart deleted file mode 100644 index 4105b4e..0000000 --- a/packages/flutter_chat_view/lib/src/services/profile_service.dart +++ /dev/null @@ -1,34 +0,0 @@ -import "dart:async"; - -import "package:flutter/material.dart"; -import "package:flutter_profile/flutter_profile.dart"; - -class ChatProfileService extends ProfileService { - @override - FutureOr editProfile(User user, String key, String? value) { - throw UnimplementedError(); - } - - @override - FutureOr pageBottomAction() { - throw UnimplementedError(); - } - - @override - FutureOr uploadImage( - BuildContext context, { - // ignore: avoid_positional_boolean_parameters - required Function(bool isUploading) onUploadStateChanged, - }) { - throw UnimplementedError(); - } - - @override - FutureOr changePassword( - BuildContext context, - String currentPassword, - String newPassword, - ) { - throw UnimplementedError(); - } -} diff --git a/packages/flutter_chat_view/pubspec.yaml b/packages/flutter_chat_view/pubspec.yaml deleted file mode 100644 index 90299f2..0000000 --- a/packages/flutter_chat_view/pubspec.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Iconica -# -# SPDX-License-Identifier: GPL-3.0-or-later - -name: flutter_chat_view -description: A standard flutter package. -version: 3.1.0 - -publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub - -environment: - sdk: ">=3.1.0 <4.0.0" - flutter: ">=1.17.0" - -dependencies: - flutter: - sdk: flutter - intl: ^0.19.0 - flutter_chat_interface: - hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub - version: ^3.1.0 - cached_network_image: ^3.2.2 - flutter_image_picker: - hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub - version: ^1.0.5 - flutter_profile: - hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub - version: ^1.5.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 - -flutter: diff --git a/packages/flutter_chat_view/test/flutter_community_chat_test.dart b/packages/flutter_chat_view/test/flutter_community_chat_test.dart deleted file mode 100644 index d327631..0000000 --- a/packages/flutter_chat_view/test/flutter_community_chat_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause -// SPDX-License-Identifier: GPL-3.0-or-later - -import "package:flutter_test/flutter_test.dart"; - -void main() { - test("test", () { - expect(true, true); - }); -}