From 2b5ab5a933a1f68a839d8dd7d0d411ee4d602a7b Mon Sep 17 00:00:00 2001 From: mike doornenbal Date: Fri, 19 Jan 2024 09:23:00 +0100 Subject: [PATCH] fix: feedback --- CHANGELOG.md | 6 + README.md | 99 ++++++++++- packages/flutter_chat/lib/flutter_chat.dart | 1 + .../src/flutter_chat_navigator_userstory.dart | 160 ++++++++++++++++++ .../lib/src/flutter_chat_userstory.dart | 2 +- .../lib/src/models/chat_configuration.dart | 2 - .../firebase_chat_overview_service.dart | 14 ++ .../lib/service/firebase_chat_service.dart | 75 ++++++++ .../lib/service/service.dart | 1 + .../lib/src/services/profile_service.dart | 1 + 10 files changed, 351 insertions(+), 10 deletions(-) create mode 100644 packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart create mode 100644 packages/flutter_chat_firebase/lib/service/firebase_chat_service.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 17b4b0b..97ac6f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.0.0 + +- Added pagination for the ChatDetailScreen +- Added routes with Go_router and Navigator +- Added ChatEntryWidget + ## 0.6.0 - December 1 2023 - Made the message controller nullable diff --git a/README.md b/README.md index bb0fe2e..3caef3a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ To use this package, add flutter_chat as a dependency in your pubspec.yaml file: path: packages/flutter_chat ``` -If you are going to use Firebase as the back-end of the Community Chat, you should also add the following package as a dependency to your pubspec.yaml file: +If you are going to use Firebase as the back-end of the Chat, you should also add the following package as a dependency to your pubspec.yaml file: ``` flutter_chat_firebase: @@ -29,6 +29,35 @@ If you are going to use Firebase as the back-end of the Community Chat, you shou Create a Firebase project for your application and add firebase firestore and storage. +make sure you are authenticated using the `Firebase_auth` package or adjust your firebase rules, otherwise you won't be able to retreive data. + +Also make sure you have the corresponding collections in your firebase project as defined in `FirebaseChatOptions`, you can override the +default paths as you wish, also the structure of your data should be equal to our predefined models, you can implement any model by making your own model and implementing one of the predefined interfaces like so: + +``` +class ChatMessageModel implements ChatMessageModelInterface { + ChatMessageModel({ + required this.sender, + required this.timestamp, + }); + + @override + final ChatUserModel sender; + @override + final DateTime timestamp; +} +``` + +below a list of interfaces you can implement; + +`ChatUserModelInterface`, +`ChatImageMessageModelInterface`, +`ChatTextMessageModelInterface` +`ChatMessageModelInterface`, +`ChatModelInterface`, +`GroupChatModelInterface`, +`PersonalChatModelInterface`, + To use the camera or photo library to send photos add the following to your project: For ios add the following lines to your info.plist: @@ -63,6 +92,8 @@ List getChatRoutes() => getChatStoryRoutes( ); ``` +You can override any method in the `ChatUserStoryConfiguration`. + Add the `getChatRoutes()` to your go_router routes like so: ``` @@ -81,12 +112,55 @@ final GoRouter _router = GoRouter( ); ``` -To use the module within your Flutter-application without predefined `Go_router` routes add the following code to the build-method of a chosen widget: +The routes that can be used to navigate are: -The `ChatScreen` shows all chats that you currently have with their latest messages. +For routing to the `ChatScreen`: + +``` + static const String chatScreen = '/chat'; +``` + +For routing to the `ChatDetailScreen`: + +``` + static String chatDetailViewPath(String chatId) => '/chat-detail/$chatId'; + static const String chatDetailScreen = '/chat-detail/:id'; +``` + +For routing to the `NewChatScreen`: + +``` +static const String newChatScreen = '/new-chat'; +``` + +For routing to the `ChatProfileScreen`: +you can see the information about a person or group you started a chat with. +If the userId is null a group profile screen will be shown otherwise the profile of a single person will be shown. + +``` +static String chatProfileScreenPath(String chatId, String? userId) => +'/chat-profile/$chatId/$userId'; +static const String chatProfileScreen = '/chat-profile/:id/:userId'; +``` + +To use the module within your Flutter-application without predefined `Go_router` routes but with Navigator routes add the following code to the build-method of a chosen widget: + +``` +chatNavigatorUserStory( + ChatUserStoryConfiguration( + chatService: ChatService, + chatOptionsBuilder: (ctx) => const ChatOptions(), + ), + context, +); +``` + +Just like with the `Go_router` routes you can override any methods in the `ChatUserStoryConfiguration`. + +Or create your own routing using the Screens: To add the `ChatScreen` add the following code: -```` +``` ChatScreen( options: options, onPressStartChat: onPressStartChat, @@ -121,10 +195,22 @@ NewChatScreen( ); ``` +On the `ChatProfileScreen` you can see the information about a person or group you started a chat with. +If the userId is null a group profile screen will be shown otherwise the profile of a single person will be shown. + +``` +ChatProfileScreen( + chatService: chatservice, + chatId: chatId, + translations: translations, + onTapUser: onTapUser, + userId: userId, +); +``` + The `ChatEntryWidget` is a widget you can put anywhere in your app. It displays the amount of unread messages you currently have. -You can choose to add a onTap to the `ChatEntryWidget` so it routes to the `ChatScreen`, -where all your chats are shown. +You can choose to add a onTap to the `ChatEntryWidget` so it routes to the `ChatScreen`. To add the `ChatEntryWidget` add the follwoing code: @@ -158,4 +244,3 @@ If you would like to contribute to the plugin (e.g. by improving the documentati ## Author This `flutter_chat` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at -```` diff --git a/packages/flutter_chat/lib/flutter_chat.dart b/packages/flutter_chat/lib/flutter_chat.dart index 7082893..5c5a302 100644 --- a/packages/flutter_chat/lib/flutter_chat.dart +++ b/packages/flutter_chat/lib/flutter_chat.dart @@ -9,3 +9,4 @@ export 'package:flutter_chat_interface/flutter_chat_interface.dart'; export 'package:flutter_chat/src/routes.dart'; export 'package:flutter_chat/src/models/chat_configuration.dart'; export 'package:flutter_chat/src/flutter_chat_userstory.dart'; +export 'package:flutter_chat/src/flutter_chat_navigator_userstory.dart'; diff --git a/packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart b/packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart new file mode 100644 index 0000000..f66583d --- /dev/null +++ b/packages/flutter_chat/lib/src/flutter_chat_navigator_userstory.dart @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; +import 'package:flutter_chat/flutter_chat.dart'; + +Widget chatNavigatorUserStory( + ChatUserStoryConfiguration configuration, BuildContext context) { + return _chatScreenRoute(configuration, context); +} + +Widget _chatScreenRoute( + ChatUserStoryConfiguration configuration, BuildContext context) { + return ChatScreen( + service: configuration.chatService, + options: configuration.chatOptionsBuilder(context), + onNoChats: () async => await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _newChatScreenRoute( + configuration, + context, + ), + ), + ), + onPressStartChat: () async { + if (configuration.onPressStartChat != null) { + return await configuration.onPressStartChat?.call(); + } + + return await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _newChatScreenRoute( + configuration, + context, + ), + ), + ); + }, + onPressChat: (chat) async => + configuration.onPressChat?.call(context, chat) ?? + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _chatDetailScreenRoute( + configuration, + context, + chat.id!, + ), + ), + ), + onDeleteChat: (chat) => + configuration.onDeleteChat?.call(context, chat) ?? + configuration.chatService.chatOverviewService.deleteChat(chat), + deleteChatDialog: configuration.deleteChatDialog, + translations: configuration.translations, + ); +} + +Widget _chatDetailScreenRoute(ChatUserStoryConfiguration configuration, + BuildContext context, String chatId) { + return ChatDetailScreen( + pageSize: configuration.messagePageSize, + options: configuration.chatOptionsBuilder(context), + translations: configuration.translations, + service: configuration.chatService, + chatId: chatId, + onMessageSubmit: (message) async { + configuration.onMessageSubmit?.call(message) ?? + configuration.chatService.chatDetailService + .sendTextMessage(chatId: chatId, text: message); + configuration.afterMessageSent?.call(chatId); + }, + onUploadImage: (image) async { + configuration.onUploadImage?.call(image) ?? + configuration.chatService.chatDetailService + .sendImageMessage(chatId: chatId, image: image); + configuration.afterMessageSent?.call(chatId); + }, + onReadChat: (chat) => + 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 await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _chatProfileScreenRoute( + configuration, + context, + chatId, + null, + ), + ), + ); + }, + iconColor: configuration.iconColor, + ); +} + +Widget _chatProfileScreenRoute(ChatUserStoryConfiguration configuration, + BuildContext context, String chatId, String? userId) { + return ChatProfileScreen( + translations: configuration.translations, + chatService: configuration.chatService, + chatId: chatId, + userId: userId, + onTapUser: (user) async { + if (configuration.onPressUserProfile != null) { + return configuration.onPressUserProfile!.call(); + } + + return Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _chatProfileScreenRoute( + configuration, + context, + chatId, + userId, + ), + ), + ); + }, + ); +} + +Widget _newChatScreenRoute( + ChatUserStoryConfiguration configuration, BuildContext context) { + return NewChatScreen( + options: configuration.chatOptionsBuilder(context), + translations: configuration.translations, + service: configuration.chatService, + 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, + ), + ); + } + if (context.mounted) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _chatDetailScreenRoute( + configuration, + context, + chat.id!, + ), + ), + ); + } + }, + ); +} diff --git a/packages/flutter_chat/lib/src/flutter_chat_userstory.dart b/packages/flutter_chat/lib/src/flutter_chat_userstory.dart index c92b79f..e3fc275 100644 --- a/packages/flutter_chat/lib/src/flutter_chat_userstory.dart +++ b/packages/flutter_chat/lib/src/flutter_chat_userstory.dart @@ -105,7 +105,7 @@ List getChatStoryRoutes( service: configuration.chatService, onPressCreateChat: (user) async { configuration.onPressCreateChat?.call(user); - if (configuration.onPressChat != null) return; + if (configuration.onPressCreateChat != null) return; var chat = await configuration.chatService.chatOverviewService .getChatByUser(user); if (chat.id == null) { diff --git a/packages/flutter_chat/lib/src/models/chat_configuration.dart b/packages/flutter_chat/lib/src/models/chat_configuration.dart index 93c6e97..60e7078 100644 --- a/packages/flutter_chat/lib/src/models/chat_configuration.dart +++ b/packages/flutter_chat/lib/src/models/chat_configuration.dart @@ -12,7 +12,6 @@ class ChatUserStoryConfiguration { const ChatUserStoryConfiguration({ required this.chatService, required this.chatOptionsBuilder, - this.pageSize = 10, this.onPressStartChat, this.onPressChat, this.onDeleteChat, @@ -47,7 +46,6 @@ class ChatUserStoryConfiguration { /// If true, the user will be routed to the new chat screen if there are no chats. final bool routeToNewChatIfEmpty; - final int pageSize; final int messagePageSize; final Future Function(BuildContext, ChatModel)? deleteChatDialog; diff --git a/packages/flutter_chat_firebase/lib/service/firebase_chat_overview_service.dart b/packages/flutter_chat_firebase/lib/service/firebase_chat_overview_service.dart index 9510149..30e63f2 100644 --- a/packages/flutter_chat_firebase/lib/service/firebase_chat_overview_service.dart +++ b/packages/flutter_chat_firebase/lib/service/firebase_chat_overview_service.dart @@ -163,8 +163,22 @@ class FirebaseChatOverviewService implements ChatOverviewService { for (ChatModel 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(), diff --git a/packages/flutter_chat_firebase/lib/service/firebase_chat_service.dart b/packages/flutter_chat_firebase/lib/service/firebase_chat_service.dart new file mode 100644 index 0000000..c3e6d2b --- /dev/null +++ b/packages/flutter_chat_firebase/lib/service/firebase_chat_service.dart @@ -0,0 +1,75 @@ +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'; + +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, + ); + } + + final FirebaseChatOptions? options; + final FirebaseApp? app; + ChatDetailService? firebaseChatDetailService; + ChatOverviewService? firebaseChatOverviewService; + 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/service.dart b/packages/flutter_chat_firebase/lib/service/service.dart index 99331f3..4ea8aac 100644 --- a/packages/flutter_chat_firebase/lib/service/service.dart +++ b/packages/flutter_chat_firebase/lib/service/service.dart @@ -1,3 +1,4 @@ export 'package:flutter_chat_firebase/service/firebase_chat_user_service.dart'; 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'; diff --git a/packages/flutter_chat_view/lib/src/services/profile_service.dart b/packages/flutter_chat_view/lib/src/services/profile_service.dart index 644e10a..1ca431b 100644 --- a/packages/flutter_chat_view/lib/src/services/profile_service.dart +++ b/packages/flutter_chat_view/lib/src/services/profile_service.dart @@ -17,6 +17,7 @@ class ChatProfileService extends ProfileService { @override FutureOr uploadImage( BuildContext context, { + // ignore: avoid_positional_boolean_parameters required Function(bool isUploading) onUploadStateChanged, }) { throw UnimplementedError();