From 1500a3a9e22c1a97de3abc3808bd15144d5a853c Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Wed, 12 Feb 2025 13:42:09 +0100 Subject: [PATCH] feat: add FlutterChatDetailNavigatorUserstory to start a userstory with only the chat detail and subscreens of that screen --- CHANGELOG.md | 1 + .../lib/src/local/local_chat_repository.dart | 3 + .../lib/src/local/local_user_repository.dart | 3 + .../flutter_chat_navigator_userstories.dart | 98 ++++++++++++++----- packages/flutter_chat/lib/src/routes.dart | 39 ++++++-- .../chat_detail/chat_detail_screen.dart | 34 ++++--- 6 files changed, 134 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 462d50e..7b087d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Added ChatScope that can be used to get the ChatService and ChatTranslations from the context. If you use individual components instead of the userstory you need to wrap them with the ChatScope. The options and service will be removed from all the component constructors. - Added getAllUsersForChat to UserRepositoryInterface for fetching all users for a chat - Added flutter_hooks as a dependency for easier state management +- Added FlutterChatDetailNavigatorUserstory that can be used to start the userstory from the chat detail screen without having the chat overview screen ## 4.0.0 - Move to the new user story architecture diff --git a/packages/chat_repository_interface/lib/src/local/local_chat_repository.dart b/packages/chat_repository_interface/lib/src/local/local_chat_repository.dart index 74f1061..239dc64 100644 --- a/packages/chat_repository_interface/lib/src/local/local_chat_repository.dart +++ b/packages/chat_repository_interface/lib/src/local/local_chat_repository.dart @@ -238,4 +238,7 @@ class LocalChatRepository implements ChatRepositoryInterface { required Uint8List image, }) => Future.value("https://picsum.photos/200/300"); + + /// All the chats of the local memory database + List get getLocalChats => chats; } diff --git a/packages/chat_repository_interface/lib/src/local/local_user_repository.dart b/packages/chat_repository_interface/lib/src/local/local_user_repository.dart index 65c7706..d6af494 100644 --- a/packages/chat_repository_interface/lib/src/local/local_user_repository.dart +++ b/packages/chat_repository_interface/lib/src/local/local_user_repository.dart @@ -47,4 +47,7 @@ class LocalUserRepository implements UserRepositoryInterface { ) .toList(), ); + + /// All the users of the local memory database + List get getLocalUsers => users; } diff --git a/packages/flutter_chat/lib/src/flutter_chat_navigator_userstories.dart b/packages/flutter_chat/lib/src/flutter_chat_navigator_userstories.dart index c00627d..bbe8694 100644 --- a/packages/flutter_chat/lib/src/flutter_chat_navigator_userstories.dart +++ b/packages/flutter_chat/lib/src/flutter_chat_navigator_userstories.dart @@ -9,35 +9,31 @@ import "package:flutter_chat/src/routes.dart"; import "package:flutter_chat/src/services/pop_handler.dart"; import "package:flutter_chat/src/util/scope.dart"; -/// The flutter chat navigator userstory -/// [userId] is the id of the user -/// [chatOptions] are the chat options -/// This widget is the entry point for the chat UI -class FlutterChatNavigatorUserstory extends StatefulWidget { - /// Constructs a [FlutterChatNavigatorUserstory]. - const FlutterChatNavigatorUserstory({ +/// Base class for both chat navigator user stories. +abstract class BaseChatNavigatorUserstory extends StatefulWidget { + /// Constructs a [BaseChatNavigatorUserstory]. + const BaseChatNavigatorUserstory({ required this.userId, required this.options, this.onExit, super.key, }); - /// The user ID of the person currently looking at the chat + /// The user ID of the person starting the chat userstory. final String userId; - /// The chat options + /// The chat userstory configuration. final ChatOptions options; /// Callback for when the user wants to navigate back to a previous screen final VoidCallback? onExit; @override - State createState() => - _FlutterChatNavigatorUserstoryState(); + State createState(); } -class _FlutterChatNavigatorUserstoryState - extends State { +abstract class _BaseChatNavigatorUserstoryState< + T extends BaseChatNavigatorUserstory> extends State { late ChatService _service = ChatService( userId: widget.userId, chatRepository: widget.options.chatRepository, @@ -45,7 +41,6 @@ class _FlutterChatNavigatorUserstoryState ); late final PopHandler _popHandler = PopHandler(); - final GlobalKey _nestedNavigatorKey = GlobalKey(); @@ -56,26 +51,19 @@ class _FlutterChatNavigatorUserstoryState service: _service, popHandler: _popHandler, child: NavigatorPopHandler( - // ignore: deprecated_member_use onPop: () => _popHandler.handlePop(), child: Navigator( key: _nestedNavigatorKey, onGenerateRoute: (settings) => MaterialPageRoute( - builder: (context) => NavigatorWrapper( - userId: widget.userId, - chatService: _service, - chatOptions: widget.options, - onExit: widget.onExit, - ), + builder: (context) => buildInitialScreen(), ), ), ), ); @override - void didUpdateWidget(covariant FlutterChatNavigatorUserstory oldWidget) { + void didUpdateWidget(covariant T oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.userId != widget.userId || oldWidget.options != widget.options) { setState(() { @@ -87,4 +75,68 @@ class _FlutterChatNavigatorUserstoryState }); } } + + /// Implemented by subclasses to provide the initial screen of the userstory. + Widget buildInitialScreen(); +} + +/// Default Chat Userstory that starts at the chat list screen. +class FlutterChatNavigatorUserstory extends BaseChatNavigatorUserstory { + /// Constructs a [FlutterChatNavigatorUserstory]. + const FlutterChatNavigatorUserstory({ + required super.userId, + required super.options, + super.onExit, + super.key, + }); + + @override + State createState() => + _FlutterChatNavigatorUserstoryState(); +} + +class _FlutterChatNavigatorUserstoryState + extends _BaseChatNavigatorUserstoryState { + @override + Widget buildInitialScreen() => NavigatorWrapper( + userId: widget.userId, + chatService: _service, + chatOptions: widget.options, + onExit: widget.onExit, + ); +} + +/// Chat Userstory that starts directly in a chat detail screen. +class FlutterChatDetailNavigatorUserstory extends BaseChatNavigatorUserstory { + /// Constructs a [FlutterChatDetailNavigatorUserstory]. + const FlutterChatDetailNavigatorUserstory({ + required super.userId, + required super.options, + required this.chat, + super.onExit, + super.key, + }); + + /// The chat to start in. + final ChatModel chat; + + @override + State createState() => + _FlutterChatDetailNavigatorUserstoryState(); +} + +class _FlutterChatDetailNavigatorUserstoryState + extends _BaseChatNavigatorUserstoryState< + FlutterChatDetailNavigatorUserstory> { + @override + Widget buildInitialScreen() => NavigatorWrapper( + userId: widget.userId, + chatService: _service, + chatOptions: widget.options, + onExit: widget.onExit, + ).chatDetailScreen( + context, + widget.chat, + widget.onExit, + ); } diff --git a/packages/flutter_chat/lib/src/routes.dart b/packages/flutter_chat/lib/src/routes.dart index 33d21a4..3f00df6 100644 --- a/packages/flutter_chat/lib/src/routes.dart +++ b/packages/flutter_chat/lib/src/routes.dart @@ -39,8 +39,14 @@ class NavigatorWrapper extends StatelessWidget { /// The chat overview screen Widget chatScreen(BuildContext context) => ChatScreen( onExit: onExit, - onPressChat: (chat) async => - _routeToScreen(context, chatDetailScreen(context, chat)), + onPressChat: (chat) async => _routeToScreen( + context, + chatDetailScreen( + context, + chat, + () => Navigator.of(context).pop(), + ), + ), onDeleteChat: (chat) async { await chatService.deleteChat(chatId: chat.id); }, @@ -49,10 +55,14 @@ class NavigatorWrapper extends StatelessWidget { ); /// The chat screen - Widget chatDetailScreen(BuildContext context, ChatModel chat) => + Widget chatDetailScreen( + BuildContext context, + ChatModel chat, + VoidCallback? onExit, + ) => ChatDetailScreen( chat: chat, - onExit: () => Navigator.of(context).pop(), + onExit: onExit, onReadChat: (chat) async => chatService.markAsRead(chatId: chat.id), onPressChatTitle: (chat) async { if (chat.isGroupChat) { @@ -116,7 +126,14 @@ class NavigatorWrapper extends StatelessWidget { var chat = await _createChat(userId); if (!context.mounted) return; - return _routeToScreen(context, chatDetailScreen(context, chat)); + return _routeToScreen( + context, + chatDetailScreen( + context, + chat, + () => Navigator.of(context).pop(), + ), + ); }, ); @@ -131,7 +148,11 @@ class NavigatorWrapper extends StatelessWidget { if (!context.mounted) return; return _replaceCurrentScreen( context, - chatDetailScreen(context, chat), + chatDetailScreen( + context, + chat, + () => Navigator.of(context).pop(), + ), ); }, ); @@ -168,7 +189,11 @@ class NavigatorWrapper extends StatelessWidget { if (!context.mounted) return; return _replaceCurrentScreen( context, - chatDetailScreen(context, chat), + chatDetailScreen( + context, + chat, + () => Navigator.of(context).pop(), + ), ); }, ); diff --git a/packages/flutter_chat/lib/src/screens/chat_detail/chat_detail_screen.dart b/packages/flutter_chat/lib/src/screens/chat_detail/chat_detail_screen.dart index afb57ff..74db28b 100644 --- a/packages/flutter_chat/lib/src/screens/chat_detail/chat_detail_screen.dart +++ b/packages/flutter_chat/lib/src/screens/chat_detail/chat_detail_screen.dart @@ -48,7 +48,7 @@ class ChatDetailScreen extends StatefulHookWidget { final String Function(ChatModel chat)? getChatTitle; /// Callback for when the user wants to navigate back - final VoidCallback onExit; + final VoidCallback? onExit; @override State createState() => _ChatDetailScreenState(); @@ -96,6 +96,7 @@ class _ChatDetailScreenState extends State { chatTitle: chatTitle, onPressChatTitle: widget.onPressChatTitle, chatModel: widget.chat, + onPressBack: widget.onExit, ); var body = _Body( @@ -107,8 +108,9 @@ class _ChatDetailScreenState extends State { ); useEffect(() { - chatScope.popHandler.add(widget.onExit); - return () => chatScope.popHandler.remove(widget.onExit); + if (widget.onExit == null) return null; + chatScope.popHandler.add(widget.onExit!); + return () => chatScope.popHandler.remove(widget.onExit!); }); if (chatOptions.builders.baseScreenBuilder == null) { @@ -132,11 +134,13 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { required this.chatTitle, required this.onPressChatTitle, required this.chatModel, + this.onPressBack, }); final String? chatTitle; final Function(ChatModel) onPressChatTitle; final ChatModel chatModel; + final VoidCallback? onPressBack; @override Widget build(BuildContext context) { @@ -144,18 +148,20 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { var theme = Theme.of(context); return AppBar( - iconTheme: theme.appBarTheme.iconTheme ?? - const IconThemeData(color: Colors.white), + iconTheme: theme.appBarTheme.iconTheme, centerTitle: true, - leading: GestureDetector( - onTap: () { - Navigator.popUntil(context, (route) => route.isFirst); - }, - child: const Icon( - Icons.arrow_back_ios, - ), - ), - title: GestureDetector( + leading: onPressBack == null + ? null + : InkWell( + onTap: onPressBack, + child: const Icon( + Icons.arrow_back_ios, + ), + ), + title: InkWell( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + hoverColor: Colors.transparent, onTap: () => onPressChatTitle.call(chatModel), child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ?? Text(