feat: add FlutterChatDetailNavigatorUserstory to start a userstory with only the chat detail and subscreens of that screen

This commit is contained in:
Freek van de Ven 2025-02-12 13:42:09 +01:00
parent 5ec4cf2577
commit 1500a3a9e2
6 changed files with 134 additions and 44 deletions

View file

@ -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 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 getAllUsersForChat to UserRepositoryInterface for fetching all users for a chat
- Added flutter_hooks as a dependency for easier state management - 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 ## 4.0.0
- Move to the new user story architecture - Move to the new user story architecture

View file

@ -238,4 +238,7 @@ class LocalChatRepository implements ChatRepositoryInterface {
required Uint8List image, required Uint8List image,
}) => }) =>
Future.value("https://picsum.photos/200/300"); Future.value("https://picsum.photos/200/300");
/// All the chats of the local memory database
List<ChatModel> get getLocalChats => chats;
} }

View file

@ -47,4 +47,7 @@ class LocalUserRepository implements UserRepositoryInterface {
) )
.toList(), .toList(),
); );
/// All the users of the local memory database
List<UserModel> get getLocalUsers => users;
} }

View file

@ -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/services/pop_handler.dart";
import "package:flutter_chat/src/util/scope.dart"; import "package:flutter_chat/src/util/scope.dart";
/// The flutter chat navigator userstory /// Base class for both chat navigator user stories.
/// [userId] is the id of the user abstract class BaseChatNavigatorUserstory extends StatefulWidget {
/// [chatOptions] are the chat options /// Constructs a [BaseChatNavigatorUserstory].
/// This widget is the entry point for the chat UI const BaseChatNavigatorUserstory({
class FlutterChatNavigatorUserstory extends StatefulWidget {
/// Constructs a [FlutterChatNavigatorUserstory].
const FlutterChatNavigatorUserstory({
required this.userId, required this.userId,
required this.options, required this.options,
this.onExit, this.onExit,
super.key, 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; final String userId;
/// The chat options /// The chat userstory configuration.
final ChatOptions options; final ChatOptions options;
/// Callback for when the user wants to navigate back to a previous screen /// Callback for when the user wants to navigate back to a previous screen
final VoidCallback? onExit; final VoidCallback? onExit;
@override @override
State<FlutterChatNavigatorUserstory> createState() => State<BaseChatNavigatorUserstory> createState();
_FlutterChatNavigatorUserstoryState();
} }
class _FlutterChatNavigatorUserstoryState abstract class _BaseChatNavigatorUserstoryState<
extends State<FlutterChatNavigatorUserstory> { T extends BaseChatNavigatorUserstory> extends State<T> {
late ChatService _service = ChatService( late ChatService _service = ChatService(
userId: widget.userId, userId: widget.userId,
chatRepository: widget.options.chatRepository, chatRepository: widget.options.chatRepository,
@ -45,7 +41,6 @@ class _FlutterChatNavigatorUserstoryState
); );
late final PopHandler _popHandler = PopHandler(); late final PopHandler _popHandler = PopHandler();
final GlobalKey<NavigatorState> _nestedNavigatorKey = final GlobalKey<NavigatorState> _nestedNavigatorKey =
GlobalKey<NavigatorState>(); GlobalKey<NavigatorState>();
@ -56,26 +51,19 @@ class _FlutterChatNavigatorUserstoryState
service: _service, service: _service,
popHandler: _popHandler, popHandler: _popHandler,
child: NavigatorPopHandler( child: NavigatorPopHandler(
// ignore: deprecated_member_use
onPop: () => _popHandler.handlePop(), onPop: () => _popHandler.handlePop(),
child: Navigator( child: Navigator(
key: _nestedNavigatorKey, key: _nestedNavigatorKey,
onGenerateRoute: (settings) => MaterialPageRoute( onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => NavigatorWrapper( builder: (context) => buildInitialScreen(),
userId: widget.userId,
chatService: _service,
chatOptions: widget.options,
onExit: widget.onExit,
),
), ),
), ),
), ),
); );
@override @override
void didUpdateWidget(covariant FlutterChatNavigatorUserstory oldWidget) { void didUpdateWidget(covariant T oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.userId != widget.userId || if (oldWidget.userId != widget.userId ||
oldWidget.options != widget.options) { oldWidget.options != widget.options) {
setState(() { 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<BaseChatNavigatorUserstory> createState() =>
_FlutterChatNavigatorUserstoryState();
}
class _FlutterChatNavigatorUserstoryState
extends _BaseChatNavigatorUserstoryState<FlutterChatNavigatorUserstory> {
@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<BaseChatNavigatorUserstory> 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,
);
} }

View file

@ -39,8 +39,14 @@ class NavigatorWrapper extends StatelessWidget {
/// The chat overview screen /// The chat overview screen
Widget chatScreen(BuildContext context) => ChatScreen( Widget chatScreen(BuildContext context) => ChatScreen(
onExit: onExit, onExit: onExit,
onPressChat: (chat) async => onPressChat: (chat) async => _routeToScreen(
_routeToScreen(context, chatDetailScreen(context, chat)), context,
chatDetailScreen(
context,
chat,
() => Navigator.of(context).pop(),
),
),
onDeleteChat: (chat) async { onDeleteChat: (chat) async {
await chatService.deleteChat(chatId: chat.id); await chatService.deleteChat(chatId: chat.id);
}, },
@ -49,10 +55,14 @@ class NavigatorWrapper extends StatelessWidget {
); );
/// The chat screen /// The chat screen
Widget chatDetailScreen(BuildContext context, ChatModel chat) => Widget chatDetailScreen(
BuildContext context,
ChatModel chat,
VoidCallback? onExit,
) =>
ChatDetailScreen( ChatDetailScreen(
chat: chat, chat: chat,
onExit: () => Navigator.of(context).pop(), onExit: onExit,
onReadChat: (chat) async => chatService.markAsRead(chatId: chat.id), onReadChat: (chat) async => chatService.markAsRead(chatId: chat.id),
onPressChatTitle: (chat) async { onPressChatTitle: (chat) async {
if (chat.isGroupChat) { if (chat.isGroupChat) {
@ -116,7 +126,14 @@ class NavigatorWrapper extends StatelessWidget {
var chat = await _createChat(userId); var chat = await _createChat(userId);
if (!context.mounted) return; 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; if (!context.mounted) return;
return _replaceCurrentScreen( return _replaceCurrentScreen(
context, context,
chatDetailScreen(context, chat), chatDetailScreen(
context,
chat,
() => Navigator.of(context).pop(),
),
); );
}, },
); );
@ -168,7 +189,11 @@ class NavigatorWrapper extends StatelessWidget {
if (!context.mounted) return; if (!context.mounted) return;
return _replaceCurrentScreen( return _replaceCurrentScreen(
context, context,
chatDetailScreen(context, chat), chatDetailScreen(
context,
chat,
() => Navigator.of(context).pop(),
),
); );
}, },
); );

View file

@ -48,7 +48,7 @@ class ChatDetailScreen extends StatefulHookWidget {
final String Function(ChatModel chat)? getChatTitle; final String Function(ChatModel chat)? getChatTitle;
/// Callback for when the user wants to navigate back /// Callback for when the user wants to navigate back
final VoidCallback onExit; final VoidCallback? onExit;
@override @override
State<ChatDetailScreen> createState() => _ChatDetailScreenState(); State<ChatDetailScreen> createState() => _ChatDetailScreenState();
@ -96,6 +96,7 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
chatTitle: chatTitle, chatTitle: chatTitle,
onPressChatTitle: widget.onPressChatTitle, onPressChatTitle: widget.onPressChatTitle,
chatModel: widget.chat, chatModel: widget.chat,
onPressBack: widget.onExit,
); );
var body = _Body( var body = _Body(
@ -107,8 +108,9 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
); );
useEffect(() { useEffect(() {
chatScope.popHandler.add(widget.onExit); if (widget.onExit == null) return null;
return () => chatScope.popHandler.remove(widget.onExit); chatScope.popHandler.add(widget.onExit!);
return () => chatScope.popHandler.remove(widget.onExit!);
}); });
if (chatOptions.builders.baseScreenBuilder == null) { if (chatOptions.builders.baseScreenBuilder == null) {
@ -132,11 +134,13 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
required this.chatTitle, required this.chatTitle,
required this.onPressChatTitle, required this.onPressChatTitle,
required this.chatModel, required this.chatModel,
this.onPressBack,
}); });
final String? chatTitle; final String? chatTitle;
final Function(ChatModel) onPressChatTitle; final Function(ChatModel) onPressChatTitle;
final ChatModel chatModel; final ChatModel chatModel;
final VoidCallback? onPressBack;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -144,18 +148,20 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
var theme = Theme.of(context); var theme = Theme.of(context);
return AppBar( return AppBar(
iconTheme: theme.appBarTheme.iconTheme ?? iconTheme: theme.appBarTheme.iconTheme,
const IconThemeData(color: Colors.white),
centerTitle: true, centerTitle: true,
leading: GestureDetector( leading: onPressBack == null
onTap: () { ? null
Navigator.popUntil(context, (route) => route.isFirst); : InkWell(
}, onTap: onPressBack,
child: const Icon( child: const Icon(
Icons.arrow_back_ios, Icons.arrow_back_ios,
), ),
), ),
title: GestureDetector( title: InkWell(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
onTap: () => onPressChatTitle.call(chatModel), onTap: () => onPressChatTitle.call(chatModel),
child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ?? child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ??
Text( Text(