From 97c259761cec977de890d889d72c41a1ae04a9d8 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Wed, 12 Feb 2025 17:28:23 +0100 Subject: [PATCH] fix: remove NavigationWrapper to be more consistent with flutter_availability userstory --- .../flutter_chat_navigator_userstories.dart | 185 +++---- packages/flutter_chat/lib/src/routes.dart | 471 ++++++++++-------- .../lib/src/screens/chat_profile_screen.dart | 155 +++--- 3 files changed, 414 insertions(+), 397 deletions(-) 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 bbe8694..fbdc86a 100644 --- a/packages/flutter_chat/lib/src/flutter_chat_navigator_userstories.dart +++ b/packages/flutter_chat/lib/src/flutter_chat_navigator_userstories.dart @@ -1,87 +1,16 @@ // SPDX-FileCopyrightText: 2023 Iconica // // SPDX-License-Identifier: BSD-3-Clause - 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/routes.dart"; import "package:flutter_chat/src/services/pop_handler.dart"; import "package:flutter_chat/src/util/scope.dart"; - -/// 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 starting the chat userstory. - final String userId; - - /// 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(); -} - -abstract class _BaseChatNavigatorUserstoryState< - T extends BaseChatNavigatorUserstory> extends State { - late ChatService _service = ChatService( - userId: widget.userId, - chatRepository: widget.options.chatRepository, - userRepository: widget.options.userRepository, - ); - - late final PopHandler _popHandler = PopHandler(); - final GlobalKey _nestedNavigatorKey = - GlobalKey(); - - @override - Widget build(BuildContext context) => ChatScope( - userId: widget.userId, - options: widget.options, - service: _service, - popHandler: _popHandler, - child: NavigatorPopHandler( - onPop: () => _popHandler.handlePop(), - child: Navigator( - key: _nestedNavigatorKey, - onGenerateRoute: (settings) => MaterialPageRoute( - builder: (context) => buildInitialScreen(), - ), - ), - ), - ); - - @override - void didUpdateWidget(covariant T oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.userId != widget.userId || - oldWidget.options != widget.options) { - setState(() { - _service = ChatService( - userId: widget.userId, - chatRepository: widget.options.chatRepository, - userRepository: widget.options.userRepository, - ); - }); - } - } - - /// Implemented by subclasses to provide the initial screen of the userstory. - Widget buildInitialScreen(); -} +import "package:flutter_hooks/flutter_hooks.dart"; /// Default Chat Userstory that starts at the chat list screen. -class FlutterChatNavigatorUserstory extends BaseChatNavigatorUserstory { +class FlutterChatNavigatorUserstory extends _BaseChatNavigatorUserstory { /// Constructs a [FlutterChatNavigatorUserstory]. const FlutterChatNavigatorUserstory({ required super.userId, @@ -91,23 +20,21 @@ class FlutterChatNavigatorUserstory extends BaseChatNavigatorUserstory { }); @override - State createState() => - _FlutterChatNavigatorUserstoryState(); -} - -class _FlutterChatNavigatorUserstoryState - extends _BaseChatNavigatorUserstoryState { - @override - Widget buildInitialScreen() => NavigatorWrapper( - userId: widget.userId, - chatService: _service, - chatOptions: widget.options, - onExit: widget.onExit, + MaterialPageRoute buildInitialRoute( + BuildContext context, + ChatService service, + PopHandler popHandler, + ) => + chatOverviewRoute( + userId: userId, + chatService: service, + chatOptions: options, + onExit: onExit, ); } /// Chat Userstory that starts directly in a chat detail screen. -class FlutterChatDetailNavigatorUserstory extends BaseChatNavigatorUserstory { +class FlutterChatDetailNavigatorUserstory extends _BaseChatNavigatorUserstory { /// Constructs a [FlutterChatDetailNavigatorUserstory]. const FlutterChatDetailNavigatorUserstory({ required super.userId, @@ -121,22 +48,74 @@ class FlutterChatDetailNavigatorUserstory extends BaseChatNavigatorUserstory { 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, + MaterialPageRoute buildInitialRoute( + BuildContext context, + ChatService service, + PopHandler popHandler, + ) => + chatDetailRoute( + chat: chat, + userId: userId, + chatService: service, + chatOptions: options, + onExit: onExit, ); } + +/// Base hook widget for chat navigator userstories. +abstract class _BaseChatNavigatorUserstory extends HookWidget { + /// Constructs a [_BaseChatNavigatorUserstory]. + const _BaseChatNavigatorUserstory({ + required this.userId, + required this.options, + this.onExit, + super.key, + }); + + /// The user ID of the person starting the chat userstory. + final String userId; + + /// The chat userstory configuration. + final ChatOptions options; + + /// Callback for when the user wants to navigate back. + final VoidCallback? onExit; + + /// Implemented by subclasses to provide the initial route of the userstory. + MaterialPageRoute buildInitialRoute( + BuildContext context, + ChatService service, + PopHandler popHandler, + ); + + @override + Widget build(BuildContext context) { + var service = useMemoized( + () => ChatService( + userId: userId, + chatRepository: options.chatRepository, + userRepository: options.userRepository, + ), + [userId, options], + ); + + var popHandler = useMemoized(PopHandler.new, []); + var nestedNavigatorKey = useMemoized(GlobalKey.new, []); + + return ChatScope( + userId: userId, + options: options, + service: service, + popHandler: popHandler, + child: NavigatorPopHandler( + onPop: () => popHandler.handlePop(), + child: Navigator( + key: nestedNavigatorKey, + onGenerateInitialRoutes: (_, __) => [ + buildInitialRoute(context, service, popHandler), + ], + ), + ), + ); + } +} diff --git a/packages/flutter_chat/lib/src/routes.dart b/packages/flutter_chat/lib/src/routes.dart index 3f00df6..125d223 100644 --- a/packages/flutter_chat/lib/src/routes.dart +++ b/packages/flutter_chat/lib/src/routes.dart @@ -10,85 +10,56 @@ import "package:flutter_chat/src/screens/creation/new_chat_screen.dart"; import "package:flutter_chat/src/screens/creation/new_group_chat_overview.dart"; import "package:flutter_chat/src/screens/creation/new_group_chat_screen.dart"; -/// The navigator wrapper -class NavigatorWrapper extends StatelessWidget { - /// Constructs a [NavigatorWrapper]. - const NavigatorWrapper({ - required this.userId, - required this.chatService, - required this.chatOptions, - this.onExit, - super.key, - }); - - /// The user ID of the person starting the chat userstory - final String userId; - - /// The chat service containing the chat repository and user repository - final ChatService chatService; - - /// The chat userstory configuration - final ChatOptions chatOptions; - - /// Callback for when the user wants to navigate back - final VoidCallback? onExit; - - @override - Widget build(BuildContext context) => chatScreen(context); - - /// The chat overview screen - Widget chatScreen(BuildContext context) => ChatScreen( +/// Pushes the chat overview screen +MaterialPageRoute chatOverviewRoute({ + required String userId, + required ChatService chatService, + required ChatOptions chatOptions, + required VoidCallback? onExit, +}) => + MaterialPageRoute( + builder: (context) => ChatScreen( onExit: onExit, onPressChat: (chat) async => _routeToScreen( context, - chatDetailScreen( - context, - chat, - () => Navigator.of(context).pop(), - ), + chatDetailRoute( + chat: chat, + userId: userId, + chatService: chatService, + chatOptions: chatOptions, + onExit: () => Navigator.of(context).pop(), + ).builder(context), ), - onDeleteChat: (chat) async { - await chatService.deleteChat(chatId: chat.id); - }, - onPressStartChat: () async => - _routeToScreen(context, newChatScreen(context)), - ); + onDeleteChat: (chat) async => chatService.deleteChat(chatId: chat.id), + onPressStartChat: () async => _routeToScreen( + context, + _newChatRoute( + userId: userId, + chatService: chatService, + chatOptions: chatOptions, + ).builder(context), + ), + ), + ); - /// The chat screen - Widget chatDetailScreen( - BuildContext context, - ChatModel chat, - VoidCallback? onExit, - ) => - ChatDetailScreen( +/// Pushes the chat detail screen +MaterialPageRoute chatDetailRoute({ + required ChatModel chat, + required String userId, + required ChatService chatService, + required ChatOptions chatOptions, + required VoidCallback? onExit, +}) => + MaterialPageRoute( + builder: (context) => ChatDetailScreen( chat: chat, onExit: onExit, onReadChat: (chat) async => chatService.markAsRead(chatId: chat.id), - onPressChatTitle: (chat) async { - if (chat.isGroupChat) { - return _routeToScreen( - context, - chatProfileScreen(context, null, chat), - ); - } - - var otherUserId = chat.getOtherUser(userId); - var otherUser = await chatService.getUser(userId: otherUserId).first; - - if (!context.mounted) return; - return _routeToScreen( - context, - chatProfileScreen(context, otherUser, null), - ); - }, - onPressUserProfile: (user) async => - _routeToScreen(context, chatProfileScreen(context, user, null)), onUploadImage: (data) async { var path = await chatService.uploadImage( path: "chats/${chat.id}-$userId-${DateTime.now()}", image: data, ); - await chatService.sendMessage( messageId: "${chat.id}-$userId-${DateTime.now()}", chatId: chat.id, @@ -104,71 +75,153 @@ class NavigatorWrapper extends StatelessWidget { text: text, ); }, - ); + onPressChatTitle: (chat) async { + if (chat.isGroupChat) { + await _routeToScreen( + context, + _chatProfileRoute( + userId: userId, + chatService: chatService, + chatOptions: chatOptions, + chat: chat, + onExit: () => Navigator.of(context).pop(), + ).builder(context), + ); + } else { + var otherUserId = chat.getOtherUser(userId); + var otherUser = + await chatService.getUser(userId: otherUserId).first; + if (!context.mounted) return; + await _routeToScreen( + context, + _chatProfileRoute( + userId: userId, + chatService: chatService, + chatOptions: chatOptions, + user: otherUser, + onExit: () => Navigator.of(context).pop(), + ).builder(context), + ); + } + }, + onPressUserProfile: (user) async => _routeToScreen( + context, + _chatProfileRoute( + userId: userId, + chatService: chatService, + chatOptions: chatOptions, + user: user, + onExit: () => Navigator.of(context).pop(), + ).builder(context), + ), + ), + ); - /// The chat profile screen - Widget chatProfileScreen( - BuildContext context, - UserModel? user, - ChatModel? chat, - ) => - ChatProfileScreen( +MaterialPageRoute _chatProfileRoute({ + required String userId, + required ChatService chatService, + required ChatOptions chatOptions, + required VoidCallback onExit, + UserModel? user, + ChatModel? chat, +}) => + MaterialPageRoute( + builder: (context) => ChatProfileScreen( userModel: user, chatModel: chat, - onExit: () => Navigator.of(context).pop(), + onExit: onExit, onTapUser: (userId) async { var user = await chatService.getUser(userId: userId).first; - if (!context.mounted) return; - await _routeToScreen(context, chatProfileScreen(context, user, null)); + await _routeToScreen( + context, + _chatProfileRoute( + userId: userId, + chatService: chatService, + chatOptions: chatOptions, + user: user, + onExit: () => Navigator.of(context).pop(), + ).builder(context), + ); }, onPressStartChat: (userId) async { - var chat = await _createChat(userId); - + var chat = await _createChat(userId, chatService, userId); if (!context.mounted) return; - return _routeToScreen( + await _routeToScreen( context, - chatDetailScreen( - context, - chat, - () => Navigator.of(context).pop(), - ), + chatDetailRoute( + chat: chat, + userId: userId, + chatService: chatService, + chatOptions: chatOptions, + onExit: () => Navigator.of(context).pop(), + ).builder(context), ); }, - ); + ), + ); - /// The new chat screen - Widget newChatScreen(BuildContext context) => NewChatScreen( +MaterialPageRoute _newChatRoute({ + required String userId, + required ChatService chatService, + required ChatOptions chatOptions, +}) => + MaterialPageRoute( + builder: (context) => NewChatScreen( onExit: () => Navigator.of(context).pop(), - onPressCreateGroupChat: () async => - _routeToScreen(context, newGroupChatScreen(context)), + onPressCreateGroupChat: () async => _routeToScreen( + context, + _newGroupChatRoute( + userId: userId, + chatService: chatService, + chatOptions: chatOptions, + ).builder(context), + ), onPressCreateChat: (user) async { - var chat = await _createChat(user.id); - + var chat = await _createChat(user.id, chatService, userId); if (!context.mounted) return; - return _replaceCurrentScreen( + await _replaceCurrentScreen( context, - chatDetailScreen( - context, - chat, - () => Navigator.of(context).pop(), - ), + chatDetailRoute( + chat: chat, + userId: userId, + chatService: chatService, + chatOptions: chatOptions, + onExit: () => Navigator.of(context).pop(), + ).builder(context), ); }, - ); + ), + ); - /// The new group chat screen - Widget newGroupChatScreen(BuildContext context) => NewGroupChatScreen( +MaterialPageRoute _newGroupChatRoute({ + required String userId, + required ChatService chatService, + required ChatOptions chatOptions, +}) => + MaterialPageRoute( + builder: (context) => NewGroupChatScreen( onExit: () => Navigator.of(context).pop(), onContinue: (users) async => _replaceCurrentScreen( context, - newGroupChatOverview(context, users), + _newGroupChatOverviewRoute( + userId: userId, + chatService: chatService, + chatOptions: chatOptions, + users: users, + ).builder(context), ), - ); + ), + ); - /// The new group chat overview screen - Widget newGroupChatOverview(BuildContext context, List users) => - NewGroupChatOverview( +MaterialPageRoute _newGroupChatOverviewRoute({ + required String userId, + required ChatService chatService, + required ChatOptions chatOptions, + required List users, +}) => + MaterialPageRoute( + builder: (context) => NewGroupChatOverview( users: users, onExit: () => Navigator.of(context).pop(), onComplete: (users, title, description, image) async { @@ -184,116 +237,114 @@ class NavigatorWrapper extends StatelessWidget { title, description, path, + chatService, + userId, ); - if (!context.mounted) return; - return _replaceCurrentScreen( + await _replaceCurrentScreen( context, - chatDetailScreen( - context, - chat, - () => Navigator.of(context).pop(), - ), + chatDetailRoute( + chat: chat, + userId: userId, + chatService: chatService, + chatOptions: chatOptions, + onExit: () => Navigator.of(context).pop(), + ).builder(context), ); }, - ); + ), + ); - /// Creates a group chat - Future _createGroupChat( - List userModels, - String title, - String description, - String? imageUrl, - ) async { - ChatModel? chat; - try { - chat = await chatService.getGroupChatByUser( - currentUser: userId, - otherUsers: userModels, - chatName: title, - description: description, - ); - } on Exception catch (_) { - chat = null; - } - - if (chat == null) { - var currentUser = await chatService.getUser(userId: userId).first; - var otherUsers = await Future.wait( - userModels.map((e) => chatService.getUser(userId: e.id).first), - ); - - await chatService.createChat( - isGroupChat: true, - users: [currentUser, ...otherUsers], - chatName: title, - description: description, - imageUrl: imageUrl, - ); - - var chat = await chatService.getGroupChatByUser( - currentUser: userId, - otherUsers: otherUsers, - chatName: title, - description: description, - ); - - if (chat == null) { - throw Exception("Chat not created"); - } - - return chat; - } - - return chat; +/// Helper function to create a chat +Future _createChat( + String otherUserId, + ChatService chatService, + String userId, +) async { + ChatModel? chat; + try { + chat = await chatService.getChatByUser( + currentUser: userId, + otherUser: otherUserId, + ); + } on Exception catch (_) { + chat = null; } - - /// Creates a chat - Future _createChat(String otherUserId) async { - ChatModel? chat; - - try { - chat = await chatService.getChatByUser( - currentUser: userId, - otherUser: otherUserId, - ); - } on Exception catch (_) { - chat = null; - } - - if (chat == null) { - var currentUser = await chatService.getUser(userId: userId).first; - var otherUser = await chatService.getUser(userId: otherUserId).first; - - await chatService.createChat( - isGroupChat: false, - users: [currentUser, otherUser], - ); - - var chat = await chatService.getChatByUser( - currentUser: userId, - otherUser: otherUserId, - ); - - if (chat == null) { - throw Exception("Chat not created"); - } - - return chat; - } - - return chat; + if (chat == null) { + await chatService.createChat( + isGroupChat: false, + users: [ + await chatService.getUser(userId: userId).first, + await chatService.getUser(userId: otherUserId).first, + ], + ); + chat = await chatService.getChatByUser( + currentUser: userId, + otherUser: otherUserId, + ); + if (chat == null) throw Exception("Chat not created"); } - - /// Routes to a new screen for the userstory - Future _routeToScreen(BuildContext context, Widget screen) async => - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => screen), - ); - - /// Replaces the current screen with a new screen for the userstory - Future _replaceCurrentScreen(BuildContext context, Widget screen) async => - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => screen), - ); + return chat; } + +/// Helper function to create a group chat +Future _createGroupChat( + List userModels, + String title, + String description, + String? imageUrl, + ChatService chatService, + String userId, +) async { + ChatModel? chat; + try { + chat = await chatService.getGroupChatByUser( + currentUser: userId, + otherUsers: userModels, + chatName: title, + description: description, + ); + } on Exception catch (_) { + chat = null; + } + + if (chat == null) { + var currentUser = await chatService.getUser(userId: userId).first; + var otherUsers = await Future.wait( + userModels.map((e) => chatService.getUser(userId: e.id).first), + ); + + await chatService.createChat( + isGroupChat: true, + users: [currentUser, ...otherUsers], + chatName: title, + description: description, + imageUrl: imageUrl, + ); + + chat = await chatService.getGroupChatByUser( + currentUser: userId, + otherUsers: otherUsers, + chatName: title, + description: description, + ); + + if (chat == null) { + throw Exception("Group chat not created"); + } + } + + return chat; +} + +/// Routes to a new screen for the userstory +Future _routeToScreen(BuildContext context, Widget screen) async => + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => screen), + ); + +/// Replaces the current screen with a new screen for the userstory +Future _replaceCurrentScreen(BuildContext context, Widget screen) async => + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => screen), + ); diff --git a/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart b/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart index eaaf7c4..9bd130e 100644 --- a/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart +++ b/packages/flutter_chat/lib/src/screens/chat_profile_screen.dart @@ -47,42 +47,34 @@ class ChatProfileScreen extends HookWidget { return () => chatScope.popHandler.remove(onExit); }); + var appBar = _AppBar( + user: userModel, + chat: chatModel, + options: options, + ); + + var body = _Body( + currentUser: userId, + options: options, + service: service, + user: userModel, + chat: chatModel, + onTapUser: onTapUser, + onPressStartChat: onPressStartChat, + ); + if (options.builders.baseScreenBuilder == null) { return Scaffold( - appBar: _AppBar( - user: userModel, - chat: chatModel, - options: options, - ), - body: _Body( - currentUser: userId, - options: options, - service: service, - user: userModel, - chat: chatModel, - onTapUser: onTapUser, - onPressStartChat: onPressStartChat, - ), + appBar: appBar, + body: body, ); } return options.builders.baseScreenBuilder!.call( context, mapScreenType, - _AppBar( - user: userModel, - chat: chatModel, - options: options, - ), - _Body( - currentUser: userId, - options: options, - service: service, - user: userModel, - chat: chatModel, - onTapUser: onTapUser, - onPressStartChat: onPressStartChat, - ), + appBar, + body, ); } } @@ -102,8 +94,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { var theme = Theme.of(context); return AppBar( - iconTheme: theme.appBarTheme.iconTheme ?? - const IconThemeData(color: Colors.white), + iconTheme: theme.appBarTheme.iconTheme, title: Text( user != null ? "${user!.fullname}" @@ -143,57 +134,58 @@ class _Body extends StatelessWidget { var chatUserDisplay = Wrap( children: [ - ...chat!.users.map( - (tappedUser) => Padding( - padding: const EdgeInsets.only( - bottom: 8, - right: 8, - ), - child: InkWell( - onTap: () { - onTapUser?.call(tappedUser); - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - FutureBuilder( - future: service.getUser(userId: tappedUser).first, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const CircularProgressIndicator(); - } + if (chat != null) ...[ + ...chat!.users.map( + (tappedUser) => Padding( + padding: const EdgeInsets.only( + bottom: 8, + right: 8, + ), + child: InkWell( + onTap: () => onTapUser?.call(tappedUser), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FutureBuilder( + future: service.getUser(userId: tappedUser).first, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.waiting) { + return const CircularProgressIndicator(); + } - var user = snapshot.data; + var user = snapshot.data; - if (user == null) { - return const SizedBox.shrink(); - } + if (user == null) { + return const SizedBox.shrink(); + } - return options.builders.userAvatarBuilder?.call( - context, - user, - 44, - ) ?? - Avatar( - boxfit: BoxFit.cover, - user: User( - firstName: user.firstName, - lastName: user.lastName, - imageUrl: - user.imageUrl != null || user.imageUrl != "" - ? user.imageUrl - : null, - ), - size: 60, - ); - }, - ), - ], + return options.builders.userAvatarBuilder?.call( + context, + user, + 44, + ) ?? + Avatar( + boxfit: BoxFit.cover, + user: User( + firstName: user.firstName, + lastName: user.lastName, + imageUrl: + user.imageUrl != null || user.imageUrl != "" + ? user.imageUrl + : null, + ), + size: 60, + ); + }, + ), + ], + ), ), ), ), - ), + ], ], ); @@ -210,6 +202,7 @@ class _Body extends StatelessWidget { firstName: options.translations.groupNameEmpty, ), ) as UserModel; + return Stack( children: [ ListView( @@ -268,23 +261,17 @@ class _Body extends StatelessWidget { options.translations.groupProfileBioHeader, style: theme.textTheme.titleMedium, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Text( chat!.description ?? "", style: theme.textTheme.bodyMedium, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Text( options.translations.chatProfileUsers, style: theme.textTheme.titleMedium, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), chatUserDisplay, ], ),