From 4760e281eea49cc84ab9f2158094aae070cbec56 Mon Sep 17 00:00:00 2001 From: mike doornenbal Date: Mon, 18 Dec 2023 16:09:11 +0100 Subject: [PATCH] feat: improve user story --- .../lib/flutter_community_chat.dart | 3 + .../src/flutter_community_chat_userstory.dart | 130 ++++++++++ .../lib/src/go_router.dart | 30 +++ .../models/community_chat_configuration.dart | 57 +++++ .../lib/src/routes.dart | 10 + packages/flutter_community_chat/pubspec.yaml | 1 + .../lib/service/firebase_chat_service.dart | 3 +- .../lib/src/service/chat_service.dart | 3 +- .../example/lib/main.dart | 64 ++--- .../lib/src/screens/chat_detail_screen.dart | 196 ++++++++------- .../lib/src/screens/chat_screen.dart | 232 ++++++++++-------- .../lib/src/screens/new_chat_screen.dart | 157 +++++++----- 12 files changed, 590 insertions(+), 296 deletions(-) create mode 100644 packages/flutter_community_chat/lib/src/flutter_community_chat_userstory.dart create mode 100644 packages/flutter_community_chat/lib/src/go_router.dart create mode 100644 packages/flutter_community_chat/lib/src/models/community_chat_configuration.dart create mode 100644 packages/flutter_community_chat/lib/src/routes.dart diff --git a/packages/flutter_community_chat/lib/flutter_community_chat.dart b/packages/flutter_community_chat/lib/flutter_community_chat.dart index 2a9ff29..98b5f46 100644 --- a/packages/flutter_community_chat/lib/flutter_community_chat.dart +++ b/packages/flutter_community_chat/lib/flutter_community_chat.dart @@ -6,3 +6,6 @@ library flutter_community_chat; export 'package:flutter_community_chat_view/flutter_community_chat_view.dart'; export 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart'; +export 'package:flutter_community_chat/src/routes.dart'; +export 'package:flutter_community_chat/src/models/community_chat_configuration.dart'; +export 'package:flutter_community_chat/src/flutter_community_chat_userstory.dart'; diff --git a/packages/flutter_community_chat/lib/src/flutter_community_chat_userstory.dart b/packages/flutter_community_chat/lib/src/flutter_community_chat_userstory.dart new file mode 100644 index 0000000..9ed3965 --- /dev/null +++ b/packages/flutter_community_chat/lib/src/flutter_community_chat_userstory.dart @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; +import 'package:flutter_community_chat/src/models/community_chat_configuration.dart'; +import 'package:flutter_community_chat/src/go_router.dart'; +import 'package:flutter_community_chat/src/routes.dart'; +import 'package:flutter_community_chat_view/flutter_community_chat_view.dart'; +import 'package:go_router/go_router.dart'; + +List getCommunityChatStoryRoutes( + CommunityChatUserStoryConfiguration configuration, +) => + [ + GoRoute( + path: CommunityChatUserStoryRoutes.chatScreen, + pageBuilder: (context, state) { + var chatScreen = ChatScreen( + service: configuration.service, + options: configuration.chatOptionsBuilder(context), + onNoChats: () => + context.push(CommunityChatUserStoryRoutes.newChatScreen), + onPressStartChat: () => + configuration.onPressStartChat?.call() ?? + context.push(CommunityChatUserStoryRoutes.newChatScreen), + onPressChat: (chat) => + configuration.onPressChat?.call(context, chat) ?? + context.push( + CommunityChatUserStoryRoutes.chatDetailViewPath(chat.id!)), + onDeleteChat: (chat) => + configuration.onDeleteChat?.call(context, chat), + deleteChatDialog: configuration.deleteChatDialog, + translations: configuration.translations, + ); + return buildScreenWithoutTransition( + context: context, + state: state, + child: configuration.chatPageBuilder?.call( + context, + chatScreen, + ) ?? + Scaffold( + body: chatScreen, + ), + ); + }, + ), + GoRoute( + path: CommunityChatUserStoryRoutes.chatDetailScreen, + pageBuilder: (context, state) { + var chatId = state.pathParameters['id']; + var chat = PersonalChatModel(user: ChatUserModel(), id: chatId); + var chatDetailScreen = ChatDetailScreen( + options: configuration.chatOptionsBuilder(context), + translations: configuration.translations, + chatUserService: configuration.userService, + service: configuration.service, + messageService: configuration.messageService, + chat: chat, + onMessageSubmit: (message) async { + configuration.onMessageSubmit?.call(message) ?? + configuration.messageService + .sendTextMessage(chat: chat, text: message); + configuration.afterMessageSent?.call(chat); + }, + onUploadImage: (image) async { + configuration.onUploadImage?.call(image) ?? + configuration.messageService + .sendImageMessage(chat: chat, image: image); + configuration.afterMessageSent?.call(chat); + }, + onReadChat: (chat) => + configuration.onReadChat?.call(chat) ?? + configuration.service.readChat(chat), + onPressChatTitle: (context, chat) => + configuration.onPressChatTitle?.call(context, chat), + iconColor: configuration.iconColor, + ); + return buildScreenWithoutTransition( + context: context, + state: state, + child: configuration.chatPageBuilder?.call( + context, + chatDetailScreen, + ) ?? + Scaffold( + body: chatDetailScreen, + ), + ); + }, + ), + GoRoute( + path: CommunityChatUserStoryRoutes.newChatScreen, + pageBuilder: (context, state) { + var newChatScreen = NewChatScreen( + options: configuration.chatOptionsBuilder(context), + translations: configuration.translations, + service: configuration.service, + userService: configuration.userService, + onPressCreateChat: (user) async { + configuration.onPressCreateChat?.call(user); + if (configuration.onPressChat != null) return; + var chat = await configuration.service.getChatByUser(user); + if (chat.id == null) { + chat = await configuration.service.storeChatIfNot( + PersonalChatModel( + user: user, + ), + ); + } + if (context.mounted) { + context.push(CommunityChatUserStoryRoutes.chatDetailViewPath( + chat.id ?? '')); + } + }); + return buildScreenWithoutTransition( + context: context, + state: state, + child: configuration.chatPageBuilder?.call( + context, + newChatScreen, + ) ?? + Scaffold( + body: newChatScreen, + ), + ); + }, + ), + ]; diff --git a/packages/flutter_community_chat/lib/src/go_router.dart b/packages/flutter_community_chat/lib/src/go_router.dart new file mode 100644 index 0000000..c9113db --- /dev/null +++ b/packages/flutter_community_chat/lib/src/go_router.dart @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +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), + ); + +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_community_chat/lib/src/models/community_chat_configuration.dart b/packages/flutter_community_chat/lib/src/models/community_chat_configuration.dart new file mode 100644 index 0000000..8285517 --- /dev/null +++ b/packages/flutter_community_chat/lib/src/models/community_chat_configuration.dart @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_community_chat_view/flutter_community_chat_view.dart'; + +@immutable +class CommunityChatUserStoryConfiguration { + const CommunityChatUserStoryConfiguration({ + required this.userService, + required this.messageService, + required this.service, + required this.chatOptionsBuilder, + this.onPressStartChat, + this.onPressChat, + this.onDeleteChat, + this.onMessageSubmit, + this.onReadChat, + this.onUploadImage, + this.onPressCreateChat, + this.iconColor, + this.deleteChatDialog, + this.disableDismissForPermanentChats = false, + this.routeToNewChatIfEmpty = true, + this.translations = const ChatTranslations(), + this.chatPageBuilder, + this.onPressChatTitle, + this.afterMessageSent, + }); + final ChatService service; + final ChatUserService userService; + final MessageService messageService; + final Function(BuildContext, ChatModel)? onPressChat; + final Function(BuildContext, ChatModel)? onDeleteChat; + final ChatTranslations translations; + final bool disableDismissForPermanentChats; + final Future Function(Uint8List image)? onUploadImage; + 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(ChatModel chat)? afterMessageSent; + final Future Function(ChatModel chat)? onReadChat; + final Function(ChatUserModel)? onPressCreateChat; + 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; + + final Future Function(BuildContext, ChatModel)? deleteChatDialog; + final Function(BuildContext context, ChatModel chat)? onPressChatTitle; + final Color? iconColor; + final Widget Function(BuildContext context, Widget child)? chatPageBuilder; + final Function()? onPressStartChat; +} diff --git a/packages/flutter_community_chat/lib/src/routes.dart b/packages/flutter_community_chat/lib/src/routes.dart new file mode 100644 index 0000000..d77e8aa --- /dev/null +++ b/packages/flutter_community_chat/lib/src/routes.dart @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2023 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +mixin CommunityChatUserStoryRoutes { + static const String chatScreen = '/chat'; + static String chatDetailViewPath(String chatId) => '/chat-detail/$chatId'; + static const String chatDetailScreen = '/chat-detail/:id'; + static const String newChatScreen = '/new-chat'; +} diff --git a/packages/flutter_community_chat/pubspec.yaml b/packages/flutter_community_chat/pubspec.yaml index 33f95d7..30ebdc6 100644 --- a/packages/flutter_community_chat/pubspec.yaml +++ b/packages/flutter_community_chat/pubspec.yaml @@ -15,6 +15,7 @@ environment: dependencies: flutter: sdk: flutter + go_router: ^12.1.1 flutter_community_chat_view: git: url: https://github.com/Iconica-Development/flutter_community_chat diff --git a/packages/flutter_community_chat_firebase/lib/service/firebase_chat_service.dart b/packages/flutter_community_chat_firebase/lib/service/firebase_chat_service.dart index 175d23e..38a9c15 100644 --- a/packages/flutter_community_chat_firebase/lib/service/firebase_chat_service.dart +++ b/packages/flutter_community_chat_firebase/lib/service/firebase_chat_service.dart @@ -234,6 +234,7 @@ class FirebaseChatService implements ChatService { controller = StreamController( onListen: () async { var currentUser = await _userService.getCurrentUser(); + var userSnapshot = await _db .collection(_options.usersCollectionName) .doc(currentUser?.id) @@ -264,7 +265,7 @@ class FirebaseChatService implements ChatService { } @override - Future getOrCreateChatByUser(ChatUserModel user) async { + Future getChatByUser(ChatUserModel user) async { var currentUser = await _userService.getCurrentUser(); var collection = await _db .collection(_options.usersCollectionName) diff --git a/packages/flutter_community_chat_interface/lib/src/service/chat_service.dart b/packages/flutter_community_chat_interface/lib/src/service/chat_service.dart index d6c649d..be0248f 100644 --- a/packages/flutter_community_chat_interface/lib/src/service/chat_service.dart +++ b/packages/flutter_community_chat_interface/lib/src/service/chat_service.dart @@ -2,8 +2,7 @@ import 'package:flutter_community_chat_interface/flutter_community_chat_interfac abstract class ChatService { Stream> getChatsStream(); - @Deprecated('Use getChatById instead') - Future getOrCreateChatByUser(ChatUserModel user); + Future getChatByUser(ChatUserModel user); Future getChatById(String id); Future deleteChat(ChatModel chat); Future readChat(ChatModel chat); diff --git a/packages/flutter_community_chat_view/example/lib/main.dart b/packages/flutter_community_chat_view/example/lib/main.dart index ee9aed6..0aca106 100644 --- a/packages/flutter_community_chat_view/example/lib/main.dart +++ b/packages/flutter_community_chat_view/example/lib/main.dart @@ -115,37 +115,37 @@ class _MyStatefulWidgetState extends State { @override Widget build(BuildContext context) { - var options = const ChatOptions(); - return ChatScreen( - chats: chatStream, - options: options, - onPressChat: (chat) => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ChatDetailScreen( - userId: 'piet', - chat: chat, - chatMessages: messageStream, - options: options, - onMessageSubmit: (text) async { - return Future.delayed( - const Duration( - milliseconds: 500, - ), - () => debugPrint('onMessageSubmit'), - ); - }, - onReadChat: (chat) async {}, - onUploadImage: (image) async {}, - ), - ), - ), - onDeleteChat: (chat) => Future.delayed( - const Duration( - milliseconds: 500, - ), - () => debugPrint('onDeleteChat'), - ), - onPressStartChat: () => debugPrint('onPressStartChat'), - ); + // return ChatScreen( + // service: , + // options: options, + // onPressChat: (chat) => Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => ChatDetailScreen( + // userId: 'piet', + // chat: chat, + // chatMessages: messageStream, + // options: options, + // onMessageSubmit: (text) async { + // return Future.delayed( + // const Duration( + // milliseconds: 500, + // ), + // () => debugPrint('onMessageSubmit'), + // ); + // }, + // onReadChat: (chat) async {}, + // onUploadImage: (image) async {}, + // ), + // ), + // ), + // onDeleteChat: (chat) => Future.delayed( + // const Duration( + // milliseconds: 500, + // ), + // () => debugPrint('onDeleteChat'), + // ), + // onPressStartChat: () => debugPrint('onPressStartChat'), + // ); + return const Text('Example '); } } diff --git a/packages/flutter_community_chat_view/lib/src/screens/chat_detail_screen.dart b/packages/flutter_community_chat_view/lib/src/screens/chat_detail_screen.dart index 3c9fe7c..1f7b324 100644 --- a/packages/flutter_community_chat_view/lib/src/screens/chat_detail_screen.dart +++ b/packages/flutter_community_chat_view/lib/src/screens/chat_detail_screen.dart @@ -13,14 +13,15 @@ import 'package:flutter_community_chat_view/src/components/image_loading_snackba class ChatDetailScreen extends StatefulWidget { const ChatDetailScreen({ - required this.userId, required this.options, required this.onMessageSubmit, required this.onUploadImage, required this.onReadChat, + required this.service, + required this.chatUserService, + required this.messageService, this.translations = const ChatTranslations(), this.chat, - this.chatMessages, this.onPressChatTitle, this.iconColor, this.showTime = false, @@ -30,21 +31,22 @@ class ChatDetailScreen extends StatefulWidget { final ChatModel? chat; /// The id of the current user that is viewing the chat. - final String userId; final ChatOptions options; final ChatTranslations translations; - final Stream>? chatMessages; 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 VoidCallback? onPressChatTitle; + 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 ChatUserService chatUserService; + final MessageService messageService; @override State createState() => _ChatDetailScreenState(); @@ -54,17 +56,26 @@ class _ChatDetailScreenState extends State { // stream listener that needs to be disposed later StreamSubscription>? _chatMessagesSubscription; Stream>? _chatMessages; + ChatModel? chat; + ChatUserModel? currentUser; @override void initState() { super.initState(); // create a broadcast stream from the chat messages - _chatMessages = widget.chatMessages?.asBroadcastStream(); + if (widget.chat != null) { + _chatMessages = widget.messageService + .getMessagesStream(widget.chat!) + .asBroadcastStream(); + } _chatMessagesSubscription = _chatMessages?.listen((event) { // check if the last message is from the current user // if so, set the chat to read + Future.delayed(Duration.zero, () async { + currentUser = await widget.chatUserService.getCurrentUser(); + }); if (event.isNotEmpty && - event.last.sender.id != widget.userId && + event.last.sender.id != currentUser?.id && widget.chat != null) { widget.onReadChat(widget.chat!); } @@ -106,92 +117,97 @@ class _ChatDetailScreenState extends State { }, ); - return Scaffold( - appBar: AppBar( - centerTitle: true, - title: GestureDetector( - onTap: widget.onPressChatTitle, - child: Row( - mainAxisSize: MainAxisSize.min, - children: widget.chat == null - ? [] - : [ - if (widget.chat is GroupChatModel) ...[ - widget.options.groupAvatarBuilder( - (widget.chat! as GroupChatModel).title, - (widget.chat! as GroupChatModel).imageUrl, - 36.0, - ), - ] else if (widget.chat is PersonalChatModel) ...[ - widget.options.userAvatarBuilder( - (widget.chat! as PersonalChatModel).user, - 36.0, - ), - ] else - ...[], - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 15.5), - child: Text( - (widget.chat is GroupChatModel) - ? (widget.chat! as GroupChatModel).title - : (widget.chat is PersonalChatModel) - ? (widget.chat! as PersonalChatModel) - .user - .fullName ?? - widget.translations.anonymousUser - : '', - style: const TextStyle(fontSize: 18), + return FutureBuilder( + future: widget.service.getChatById(widget.chat?.id ?? ''), + builder: (context, AsyncSnapshot snapshot) { + var chatModel = snapshot.data; + + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: GestureDetector( + onTap: () => widget.onPressChatTitle?.call(context, chatModel!), + child: Row( + mainAxisSize: MainAxisSize.min, + children: widget.chat == null + ? [] + : [ + if (chatModel is GroupChatModel) ...[ + widget.options.groupAvatarBuilder( + chatModel.title, + chatModel.imageUrl, + 36.0, + ), + ] else if (chatModel is PersonalChatModel) ...[ + widget.options.userAvatarBuilder( + chatModel.user, + 36.0, + ), + ] else + ...[], + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 15.5), + child: Text( + (chatModel is GroupChatModel) + ? chatModel.title + : (chatModel is PersonalChatModel) + ? chatModel.user.fullName ?? + widget.translations.anonymousUser + : '', + style: const TextStyle(fontSize: 18), + ), + ), ), - ), - ), - ], - ), - ), - ), - body: Column( - children: [ - Expanded( - child: StreamBuilder>( - stream: _chatMessages, - builder: (BuildContext context, snapshot) { - var messages = snapshot.data ?? widget.chat?.messages ?? []; - ChatMessageModel? previousMessage; - - var messageWidgets = []; - - for (var message in messages) { - messageWidgets.add( - ChatDetailRow( - previousMessage: previousMessage, - showTime: widget.showTime, - translations: widget.translations, - message: message, - userAvatarBuilder: widget.options.userAvatarBuilder, - ), - ); - previousMessage = message; - } - - return ListView( - reverse: true, - padding: const EdgeInsets.only(top: 24.0), - children: messageWidgets.reversed.toList(), - ); - }, + ], + ), ), ), - if (widget.chat != null) - ChatBottom( - chat: widget.chat!, - messageInputBuilder: widget.options.messageInputBuilder, - onPressSelectImage: onPressSelectImage, - onMessageSubmit: widget.onMessageSubmit, - translations: widget.translations, - iconColor: widget.iconColor, - ), - ], - ), + body: Column( + children: [ + Expanded( + child: StreamBuilder>( + stream: _chatMessages, + builder: (context, snapshot) { + var messages = snapshot.data ?? chatModel?.messages ?? []; + ChatMessageModel? previousMessage; + + var messageWidgets = []; + + for (var message in messages) { + messageWidgets.add( + ChatDetailRow( + previousMessage: previousMessage, + showTime: widget.showTime, + translations: widget.translations, + message: message, + userAvatarBuilder: widget.options.userAvatarBuilder, + ), + ); + previousMessage = message; + } + + return ListView( + reverse: true, + padding: const EdgeInsets.only(top: 24.0), + children: messageWidgets.reversed.toList(), + ); + }, + ), + ), + if (chatModel != null) + ChatBottom( + chat: chatModel, + messageInputBuilder: widget.options.messageInputBuilder, + onPressSelectImage: onPressSelectImage, + onMessageSubmit: widget.onMessageSubmit, + translations: widget.translations, + iconColor: widget.iconColor, + ), + ], + ), + ); + }, ); } } diff --git a/packages/flutter_community_chat_view/lib/src/screens/chat_screen.dart b/packages/flutter_community_chat_view/lib/src/screens/chat_screen.dart index 78c66ef..d5542a4 100644 --- a/packages/flutter_community_chat_view/lib/src/screens/chat_screen.dart +++ b/packages/flutter_community_chat_view/lib/src/screens/chat_screen.dart @@ -11,12 +11,12 @@ import 'package:flutter_community_chat_view/src/services/date_formatter.dart'; class ChatScreen extends StatefulWidget { const ChatScreen({ required this.options, - required this.chats, required this.onPressStartChat, required this.onPressChat, required this.onDeleteChat, + required this.service, + this.onNoChats, this.deleteChatDialog, - this.unreadChats, this.translations = const ChatTranslations(), this.disableDismissForPermanentChats = false, super.key, @@ -24,11 +24,12 @@ class ChatScreen extends StatefulWidget { final ChatOptions options; final ChatTranslations translations; - final Stream> chats; - final Stream? unreadChats; + final ChatService service; final VoidCallback? onPressStartChat; + final VoidCallback? onNoChats; final void Function(ChatModel chat) onDeleteChat; final void Function(ChatModel chat) onPressChat; + /// Disable the swipe to dismiss feature for chats that are not deletable final bool disableDismissForPermanentChats; @@ -40,6 +41,7 @@ class ChatScreen extends StatefulWidget { class _ChatScreenState extends State { final DateFormatter _dateFormatter = DateFormatter(); + bool _hasCalledOnNoChats = false; @override Widget build(BuildContext context) { @@ -48,26 +50,24 @@ class _ChatScreenState extends State { AppBar( title: Text(translations.chatsTitle), centerTitle: true, - actions: widget.unreadChats != null - ? [ - StreamBuilder( - stream: widget.unreadChats, - builder: (BuildContext context, snapshot) => Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.only(right: 22.0), - child: Text( - '${snapshot.data ?? 0} ${translations.chatsUnread}', - style: const TextStyle( - color: Color(0xFFBBBBBB), - fontSize: 14, - ), - ), - ), + actions: [ + StreamBuilder( + stream: widget.service.getUnreadChatsCountStream(), + builder: (BuildContext context, snapshot) => Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.only(right: 22.0), + child: Text( + '${snapshot.data ?? 0} ${translations.chatsUnread}', + style: const TextStyle( + color: Color(0xFFBBBBBB), + fontSize: 14, ), ), - ] - : [], + ), + ), + ), + ], ), Column( children: [ @@ -76,113 +76,133 @@ class _ChatScreenState extends State { padding: const EdgeInsets.only(top: 15.0), children: [ StreamBuilder>( - stream: widget.chats, - builder: (BuildContext context, snapshot) => Column( - children: [ - for (ChatModel chat in snapshot.data ?? []) ...[ - Builder( - builder: (context) => !(widget.disableDismissForPermanentChats && !chat.canBeDeleted) - ? Dismissible( - confirmDismiss: (_) => - widget.deleteChatDialog - ?.call(context, chat) ?? - showModalBottomSheet( - context: context, - builder: (BuildContext context) => - Container( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - chat.canBeDeleted - ? translations - .deleteChatModalTitle - : translations - .chatCantBeDeleted, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - if (chat.canBeDeleted) + stream: widget.service.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)) { + if (widget.onNoChats != null && !_hasCalledOnNoChats) { + _hasCalledOnNoChats = true; // Set the flag to true + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onNoChats!.call(); + }); + } + } else { + _hasCalledOnNoChats = + false; // Reset the flag if there are chats + } + return Column( + children: [ + for (ChatModel chat in snapshot.data ?? []) ...[ + Builder( + builder: (context) => !(widget + .disableDismissForPermanentChats && + !chat.canBeDeleted) + ? Dismissible( + confirmDismiss: (_) => + widget.deleteChatDialog + ?.call(context, chat) ?? + showModalBottomSheet( + context: context, + builder: (BuildContext context) => + Container( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ Text( - translations - .deleteChatModalDescription, + chat.canBeDeleted + ? translations + .deleteChatModalTitle + : translations + .chatCantBeDeleted, style: const TextStyle( - fontSize: 16, + fontSize: 20, + fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - TextButton( - child: Text( - translations - .deleteChatModalCancel, - style: const TextStyle( - fontSize: 16, - ), + const SizedBox(height: 16), + if (chat.canBeDeleted) + Text( + translations + .deleteChatModalDescription, + style: const TextStyle( + fontSize: 16, ), - onPressed: () => - Navigator.of(context) - .pop(false), ), - if (chat.canBeDeleted) - ElevatedButton( - onPressed: () => - Navigator.of(context) - .pop(true), + const SizedBox(height: 16), + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + TextButton( child: Text( translations - .deleteChatModalConfirm, + .deleteChatModalCancel, style: const TextStyle( fontSize: 16, ), ), + onPressed: () => + Navigator.of(context) + .pop(false), ), - ], - ), - ], + if (chat.canBeDeleted) + ElevatedButton( + onPressed: () => + Navigator.of( + context, + ).pop(true), + child: Text( + translations + .deleteChatModalConfirm, + style: + const TextStyle( + fontSize: 16, + ), + ), + ), + ], + ), + ], + ), + ), + ), + onDismissed: (_) => + widget.onDeleteChat(chat), + background: Container( + color: Colors.red, + child: Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + translations.deleteChatButton, ), ), ), - onDismissed: (_) => widget.onDeleteChat(chat), - background: Container( - color: Colors.red, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - translations.deleteChatButton, - ), - ), ), - ), - key: ValueKey( - chat.id.toString(), - ), - child: ChatListItem( + key: ValueKey( + chat.id.toString(), + ), + child: ChatListItem( + widget: widget, + chat: chat, + translations: translations, + dateFormatter: _dateFormatter, + ), + ) + : ChatListItem( widget: widget, chat: chat, translations: translations, dateFormatter: _dateFormatter, ), - ) - : ChatListItem( - widget: widget, - chat: chat, - translations: translations, - dateFormatter: _dateFormatter, - ), - ), + ), + ], ], - ], - ), + ); + }, ), ], ), diff --git a/packages/flutter_community_chat_view/lib/src/screens/new_chat_screen.dart b/packages/flutter_community_chat_view/lib/src/screens/new_chat_screen.dart index ead3c6c..77fc819 100644 --- a/packages/flutter_community_chat_view/lib/src/screens/new_chat_screen.dart +++ b/packages/flutter_community_chat_view/lib/src/screens/new_chat_screen.dart @@ -8,15 +8,17 @@ import 'package:flutter_community_chat_view/flutter_community_chat_view.dart'; class NewChatScreen extends StatefulWidget { const NewChatScreen({ required this.options, - required this.users, required this.onPressCreateChat, + required this.service, + required this.userService, this.translations = const ChatTranslations(), super.key, }); final ChatOptions options; final ChatTranslations translations; - final List users; + final ChatService service; + final ChatUserService userService; final Function(ChatUserModel) onPressCreateChat; @override @@ -25,79 +27,104 @@ class NewChatScreen extends StatefulWidget { class _NewChatScreenState extends State { final FocusNode _textFieldFocusNode = FocusNode(); - bool _isSearching = false; - List? _filteredUsers; - - void filterUsers(String query) => setState( - () => _filteredUsers = query.isEmpty - ? null - : widget.users - .where( - (user) => - user.fullName?.toLowerCase().contains( - query.toLowerCase(), - ) ?? - false, - ) - .toList(), - ); + String query = ''; @override Widget build(BuildContext context) { - var users = _filteredUsers ?? widget.users; - return Scaffold( appBar: AppBar( - title: _isSearching - ? Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: TextField( - focusNode: _textFieldFocusNode, - onChanged: filterUsers, - decoration: InputDecoration( - hintText: widget.translations.searchPlaceholder, - ), - ), - ) - : Text(widget.translations.newChatButton), + title: _buildSearchField(), actions: [ - IconButton( - onPressed: () { - setState(() { - _isSearching = !_isSearching; - }); - - if (_isSearching) { - _textFieldFocusNode.requestFocus(); - } - }, - icon: Icon( - _isSearching ? Icons.close : Icons.search, - ), - ), + _buildSearchIcon(), ], ), - body: users.isEmpty - ? widget.options.noChatsPlaceholderBuilder(widget.translations) - : ListView( - children: [ - for (var user in users) - GestureDetector( - child: widget.options.chatRowContainerBuilder( - ChatRow( - avatar: widget.options.userAvatarBuilder( - user, - 40.0, - ), - title: - user.fullName ?? widget.translations.anonymousUser, - ), - ), - onTap: () => widget.onPressCreateChat(user), - ), - ], + body: FutureBuilder>( + future: widget.userService.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 + .noChatsPlaceholderBuilder(widget.translations); + } + }, + ), + ); + } + + Widget _buildSearchField() { + return _isSearching + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: TextField( + focusNode: _textFieldFocusNode, + onChanged: (value) { + setState(() { + query = value; + }); + }, + decoration: InputDecoration( + hintText: widget.translations.searchPlaceholder, + ), ), + ) + : Text(widget.translations.newChatButton); + } + + Widget _buildSearchIcon() { + return IconButton( + onPressed: () { + setState(() { + _isSearching = !_isSearching; + }); + + if (_isSearching) { + _textFieldFocusNode.requestFocus(); + } + }, + icon: Icon( + _isSearching ? Icons.close : Icons.search, + ), + ); + } + + Widget _buildUserList(List users) { + var filteredUsers = users + .where( + (user) => + user.fullName?.toLowerCase().contains( + query.toLowerCase(), + ) ?? + false, + ) + .toList(); + + if (filteredUsers.isEmpty) { + return widget.options.noChatsPlaceholderBuilder(widget.translations); + } + + return ListView.builder( + itemCount: filteredUsers.length, + itemBuilder: (context, index) { + var user = filteredUsers[index]; + return GestureDetector( + child: widget.options.chatRowContainerBuilder( + ChatRow( + avatar: widget.options.userAvatarBuilder( + user, + 40.0, + ), + title: user.fullName ?? widget.translations.anonymousUser, + ), + ), + onTap: () => widget.onPressCreateChat(user), + ); + }, ); } }