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 committed by FlutterJoey
parent ff28f91524
commit 4ec7da429e
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 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

View file

@ -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<ChatModel> get getLocalChats => chats;
}

View file

@ -47,4 +47,7 @@ class LocalUserRepository implements UserRepositoryInterface {
)
.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/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<FlutterChatNavigatorUserstory> createState() =>
_FlutterChatNavigatorUserstoryState();
State<BaseChatNavigatorUserstory> createState();
}
class _FlutterChatNavigatorUserstoryState
extends State<FlutterChatNavigatorUserstory> {
abstract class _BaseChatNavigatorUserstoryState<
T extends BaseChatNavigatorUserstory> extends State<T> {
late ChatService _service = ChatService(
userId: widget.userId,
chatRepository: widget.options.chatRepository,
@ -45,7 +41,6 @@ class _FlutterChatNavigatorUserstoryState
);
late final PopHandler _popHandler = PopHandler();
final GlobalKey<NavigatorState> _nestedNavigatorKey =
GlobalKey<NavigatorState>();
@ -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<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
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(),
),
);
},
);

View file

@ -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<ChatDetailScreen> createState() => _ChatDetailScreenState();
@ -96,6 +96,7 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
chatTitle: chatTitle,
onPressChatTitle: widget.onPressChatTitle,
chatModel: widget.chat,
onPressBack: widget.onExit,
);
var body = _Body(
@ -107,8 +108,9 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
);
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(