feat: use chatId instead of chat on chat_detail_screen and load chat from stream

This commit is contained in:
Freek van de Ven 2025-02-13 12:00:27 +01:00 committed by Bart Ribbers
parent cc52c78487
commit 627a78310a
4 changed files with 251 additions and 262 deletions

View file

@ -9,6 +9,7 @@
- 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 - Added FlutterChatDetailNavigatorUserstory that can be used to start the userstory from the chat detail screen without having the chat overview screen
- Changed the ChatDetailScreen to use the chatId instead of the ChatModel, the screen will now fetch the chat from the ChatService
## 4.0.0 ## 4.0.0
- Move to the new user story architecture - Move to the new user story architecture

View file

@ -39,13 +39,14 @@ class FlutterChatDetailNavigatorUserstory extends _BaseChatNavigatorUserstory {
const FlutterChatDetailNavigatorUserstory({ const FlutterChatDetailNavigatorUserstory({
required super.userId, required super.userId,
required super.options, required super.options,
required this.chat, required this.chatId,
super.onExit, super.onExit,
super.key, super.key,
}); });
/// The chat to start in. /// The identifier of the chat to start in.
final ChatModel chat; /// The [ChatModel] will be fetched from the [ChatRepository]
final String chatId;
@override @override
MaterialPageRoute buildInitialRoute( MaterialPageRoute buildInitialRoute(
@ -54,7 +55,7 @@ class FlutterChatDetailNavigatorUserstory extends _BaseChatNavigatorUserstory {
PopHandler popHandler, PopHandler popHandler,
) => ) =>
chatDetailRoute( chatDetailRoute(
chat: chat, chatId: chatId,
userId: userId, userId: userId,
chatService: service, chatService: service,
chatOptions: options, chatOptions: options,

View file

@ -23,7 +23,7 @@ MaterialPageRoute chatOverviewRoute({
onPressChat: (chat) async => _routeToScreen( onPressChat: (chat) async => _routeToScreen(
context, context,
chatDetailRoute( chatDetailRoute(
chat: chat, chatId: chat.id,
userId: userId, userId: userId,
chatService: chatService, chatService: chatService,
chatOptions: chatOptions, chatOptions: chatOptions,
@ -44,7 +44,7 @@ MaterialPageRoute chatOverviewRoute({
/// Pushes the chat detail screen /// Pushes the chat detail screen
MaterialPageRoute chatDetailRoute({ MaterialPageRoute chatDetailRoute({
required ChatModel chat, required String chatId,
required String userId, required String userId,
required ChatService chatService, required ChatService chatService,
required ChatOptions chatOptions, required ChatOptions chatOptions,
@ -52,25 +52,25 @@ MaterialPageRoute chatDetailRoute({
}) => }) =>
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ChatDetailScreen( builder: (context) => ChatDetailScreen(
chat: chat, chatId: chatId,
onExit: onExit, onExit: onExit,
onReadChat: (chat) async => chatService.markAsRead(chatId: chat.id), onReadChat: (chat) async => chatService.markAsRead(chatId: chat.id),
onUploadImage: (data) async { onUploadImage: (data) async {
var path = await chatService.uploadImage( var path = await chatService.uploadImage(
path: "chats/${chat.id}-$userId-${DateTime.now()}", path: "chats/$chatId-$userId-${DateTime.now()}",
image: data, image: data,
); );
await chatService.sendMessage( await chatService.sendMessage(
messageId: "${chat.id}-$userId-${DateTime.now()}", messageId: "$chatId-$userId-${DateTime.now()}",
chatId: chat.id, chatId: chatId,
senderId: userId, senderId: userId,
imageUrl: path, imageUrl: path,
); );
}, },
onMessageSubmit: (text) async { onMessageSubmit: (text) async {
await chatService.sendMessage( await chatService.sendMessage(
messageId: "${chat.id}-$userId-${DateTime.now()}", messageId: "$chatId-$userId-${DateTime.now()}",
chatId: chat.id, chatId: chatId,
senderId: userId, senderId: userId,
text: text, text: text,
); );
@ -150,7 +150,7 @@ MaterialPageRoute _chatProfileRoute({
await _routeToScreen( await _routeToScreen(
context, context,
chatDetailRoute( chatDetailRoute(
chat: chat, chatId: chat.id,
userId: userId, userId: userId,
chatService: chatService, chatService: chatService,
chatOptions: chatOptions, chatOptions: chatOptions,
@ -183,7 +183,7 @@ MaterialPageRoute _newChatRoute({
await _replaceCurrentScreen( await _replaceCurrentScreen(
context, context,
chatDetailRoute( chatDetailRoute(
chat: chat, chatId: chat.id,
userId: userId, userId: userId,
chatService: chatService, chatService: chatService,
chatOptions: chatOptions, chatOptions: chatOptions,
@ -244,7 +244,7 @@ MaterialPageRoute _newGroupChatOverviewRoute({
await _replaceCurrentScreen( await _replaceCurrentScreen(
context, context,
chatDetailRoute( chatDetailRoute(
chat: chat, chatId: chat.id,
userId: userId, userId: userId,
chatService: chatService, chatService: chatService,
chatOptions: chatOptions, chatOptions: chatOptions,

View file

@ -11,10 +11,10 @@ import "package:flutter_hooks/flutter_hooks.dart";
/// Chat detail screen /// Chat detail screen
/// Seen when a user clicks on a chat /// Seen when a user clicks on a chat
class ChatDetailScreen extends StatefulHookWidget { class ChatDetailScreen extends HookWidget {
/// Constructs a [ChatDetailScreen]. /// Constructs a [ChatDetailScreen].
const ChatDetailScreen({ const ChatDetailScreen({
required this.chat, required this.chatId,
required this.onExit, required this.onExit,
required this.onPressChatTitle, required this.onPressChatTitle,
required this.onPressUserProfile, required this.onPressUserProfile,
@ -25,8 +25,9 @@ class ChatDetailScreen extends StatefulHookWidget {
super.key, super.key,
}); });
/// The chat model currently being viewed /// The identifier of the chat that is being viewed.
final ChatModel chat; /// The chat will be fetched from the chat service.
final String chatId;
/// Callback function triggered when the chat title is pressed. /// Callback function triggered when the chat title is pressed.
final Function(ChatModel) onPressChatTitle; final Function(ChatModel) onPressChatTitle;
@ -49,69 +50,65 @@ class ChatDetailScreen extends StatefulHookWidget {
/// 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
State<ChatDetailScreen> createState() => _ChatDetailScreenState();
}
class _ChatDetailScreenState extends State<ChatDetailScreen> {
String? chatTitle;
@override
void initState() {
super.initState();
if (widget.chat.isGroupChat) {
chatTitle = widget.chat.chatName;
}
if (chatTitle != null) return;
WidgetsBinding.instance.addPostFrameCallback((_) async {
var chatScope = ChatScope.of(context);
if (widget.chat.isGroupChat) {
chatTitle = chatScope.options.translations.groupNameEmpty;
} else {
await _getTitle(chatScope);
}
});
}
Future<void> _getTitle(ChatScope chatScope) async {
if (widget.getChatTitle != null) {
chatTitle = widget.getChatTitle!.call(widget.chat);
} else {
var userId = widget.chat.users
.firstWhere((element) => element != chatScope.userId);
var user = await chatScope.service.getUser(userId: userId).first;
chatTitle = user.fullname ?? chatScope.options.translations.anonymousUser;
}
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var chatScope = ChatScope.of(context); var chatScope = ChatScope.of(context);
var options = chatScope.options; var options = chatScope.options;
var chatTitle = useState<String?>(null);
var chatStream = useMemoized(
() => chatScope.service.getChat(chatId: chatId),
[chatId],
);
var chatSnapshot = useStream(chatStream);
var chat = chatSnapshot.data;
useEffect(
() {
if (chat == null) return;
if (chat.isGroupChat) {
chatTitle.value = options.translations.groupNameEmpty;
} else {
unawaited(
_computeChatTitle(
chatScope: chatScope,
chat: chat,
getChatTitle: getChatTitle,
onTitleComputed: (title) => chatTitle.value = title,
),
);
}
return;
},
[chat],
);
useEffect(
() {
if (onExit == null) return null;
chatScope.popHandler.add(onExit!);
return () => chatScope.popHandler.remove(onExit!);
},
[onExit],
);
var appBar = _AppBar( var appBar = _AppBar(
chatTitle: chatTitle, chatTitle: chatTitle.value,
onPressChatTitle: widget.onPressChatTitle, onPressChatTitle: onPressChatTitle,
chatModel: widget.chat, chatModel: chat,
onPressBack: widget.onExit, onPressBack: onExit,
); );
var body = _Body( var body = _Body(
chat: widget.chat, chatId: chatId,
onPressUserProfile: widget.onPressUserProfile, chat: chat,
onUploadImage: widget.onUploadImage, onPressUserProfile: onPressUserProfile,
onMessageSubmit: widget.onMessageSubmit, onUploadImage: onUploadImage,
onReadChat: widget.onReadChat, onMessageSubmit: onMessageSubmit,
onReadChat: onReadChat,
); );
useEffect(() {
if (widget.onExit == null) return null;
chatScope.popHandler.add(widget.onExit!);
return () => chatScope.popHandler.remove(widget.onExit!);
});
if (options.builders.baseScreenBuilder == null) { if (options.builders.baseScreenBuilder == null) {
return Scaffold( return Scaffold(
appBar: appBar, appBar: appBar,
@ -121,24 +118,43 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
return options.builders.baseScreenBuilder!.call( return options.builders.baseScreenBuilder!.call(
context, context,
widget.mapScreenType, mapScreenType,
appBar, appBar,
body, body,
); );
} }
Future<void> _computeChatTitle({
required ChatScope chatScope,
required ChatModel chat,
required String? Function(ChatModel chat)? getChatTitle,
required void Function(String?) onTitleComputed,
}) async {
if (getChatTitle != null) {
onTitleComputed(getChatTitle(chat));
return;
}
var userId = chat.users.firstWhere((user) => user != chatScope.userId);
var user = await chatScope.service.getUser(userId: userId).first;
onTitleComputed(
user.fullname ?? chatScope.options.translations.anonymousUser,
);
}
} }
/// The app bar widget for the chat detail screen
class _AppBar extends StatelessWidget implements PreferredSizeWidget { class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const _AppBar({ const _AppBar({
required this.chatTitle, required this.chatTitle,
required this.onPressChatTitle,
required this.chatModel, required this.chatModel,
required this.onPressChatTitle,
this.onPressBack, this.onPressBack,
}); });
final String? chatTitle; final String? chatTitle;
final ChatModel? chatModel;
final Function(ChatModel) onPressChatTitle; final Function(ChatModel) onPressChatTitle;
final ChatModel chatModel;
final VoidCallback? onPressBack; final VoidCallback? onPressBack;
@override @override
@ -146,22 +162,28 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
var options = ChatScope.of(context).options; var options = ChatScope.of(context).options;
var theme = Theme.of(context); var theme = Theme.of(context);
VoidCallback? onPressChatTitle;
if (chatModel != null) {
onPressChatTitle = () => this.onPressChatTitle(chatModel!);
}
Widget? appBarIcon;
if (onPressBack != null) {
appBarIcon = InkWell(
onTap: onPressBack,
child: const Icon(Icons.arrow_back_ios),
);
}
return AppBar( return AppBar(
iconTheme: theme.appBarTheme.iconTheme, iconTheme: theme.appBarTheme.iconTheme,
centerTitle: true, centerTitle: true,
leading: onPressBack == null leading: appBarIcon,
? null
: InkWell(
onTap: onPressBack,
child: const Icon(
Icons.arrow_back_ios,
),
),
title: InkWell( title: InkWell(
splashColor: Colors.transparent, splashColor: Colors.transparent,
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
hoverColor: Colors.transparent, hoverColor: Colors.transparent,
onTap: () => onPressChatTitle.call(chatModel), onTap: onPressChatTitle,
child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ?? child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ??
Text( Text(
chatTitle ?? "", chatTitle ?? "",
@ -175,8 +197,11 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
Size get preferredSize => const Size.fromHeight(kToolbarHeight); Size get preferredSize => const Size.fromHeight(kToolbarHeight);
} }
class _Body extends StatefulWidget { /// Body for the chat detail screen
/// Displays messages, a scrollable list, and a bottom input field.
class _Body extends HookWidget {
const _Body({ const _Body({
required this.chatId,
required this.chat, required this.chat,
required this.onPressUserProfile, required this.onPressUserProfile,
required this.onUploadImage, required this.onUploadImage,
@ -184,88 +209,80 @@ class _Body extends StatefulWidget {
required this.onReadChat, required this.onReadChat,
}); });
final ChatModel chat; final String chatId;
final ChatModel? chat;
final Function(UserModel) onPressUserProfile; final Function(UserModel) onPressUserProfile;
final Function(Uint8List image) onUploadImage; final Function(Uint8List image) onUploadImage;
final Function(String message) onMessageSubmit; final Function(String message) onMessageSubmit;
final Function(ChatModel chat) onReadChat; final Function(ChatModel chat) onReadChat;
@override
State<_Body> createState() => _BodyState();
}
class _BodyState extends State<_Body> {
final ScrollController controller = ScrollController();
bool showIndicator = false;
late int pageSize = 20;
int page = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
var chatScope = ChatScope.of(context);
pageSize = chatScope.options.pageSize;
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var chatScope = ChatScope.of(context); var chatScope = ChatScope.of(context);
var options = chatScope.options; var options = chatScope.options;
var service = chatScope.service; var service = chatScope.service;
void handleScroll(PointerMoveEvent event) { var pageSize = useState(chatScope.options.pageSize);
if (!showIndicator && var page = useState(0);
var showIndicator = useState(false);
var controller = useScrollController();
/// Trigger to load new page when scrolling to the bottom
void handleScroll(PointerMoveEvent _) {
if (!showIndicator.value &&
controller.offset >= controller.position.maxScrollExtent && controller.offset >= controller.position.maxScrollExtent &&
!controller.position.outOfRange) { !controller.position.outOfRange) {
setState(() { showIndicator.value = true;
showIndicator = true; page.value++;
page++;
});
Future.delayed(const Duration(seconds: 2), () { Future.delayed(const Duration(seconds: 2), () {
if (!mounted) return; if (!controller.hasClients) return;
setState(() { showIndicator.value = false;
showIndicator = false;
});
}); });
} }
} }
if (chat == null) {
return const Center(child: CircularProgressIndicator());
}
var messagesStream = useMemoized(
() => service.getMessages(
chatId: chat!.id,
pageSize: pageSize.value,
page: page.value,
),
[chat!.id, pageSize.value, page.value],
);
var messagesSnapshot = useStream(messagesStream);
var messages = messagesSnapshot.data?.reversed.toList() ?? [];
if (messagesSnapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
var listViewChildren = messages.isEmpty && !showIndicator.value
? [
_ChatNoMessages(isGroupChat: chat!.isGroupChat),
]
: [
for (var (index, message) in messages.indexed)
if (chat!.id == message.chatId)
_ChatBubble(
key: ValueKey(message.id),
message: message,
previousMessage:
index < messages.length - 1 ? messages[index + 1] : null,
onPressUserProfile: onPressUserProfile,
),
];
return Stack( return Stack(
children: [ children: [
Column( Column(
children: [ children: [
Expanded( Expanded(
child: Align( child: Listener(
alignment: options.chatAlignment ?? Alignment.bottomCenter,
child: StreamBuilder<List<MessageModel>?>(
stream: service.getMessages(
chatId: widget.chat.id,
pageSize: pageSize,
page: page,
),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
var messages = snapshot.data?.reversed.toList() ?? [];
WidgetsBinding.instance.addPostFrameCallback((_) async {
await widget.onReadChat(widget.chat);
});
return Listener(
onPointerMove: handleScroll, onPointerMove: handleScroll,
child: ListView( child: ListView(
shrinkWrap: true, shrinkWrap: true,
@ -273,66 +290,47 @@ class _BodyState extends State<_Body> {
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
reverse: messages.isNotEmpty, reverse: messages.isNotEmpty,
padding: const EdgeInsets.only(top: 24.0), padding: const EdgeInsets.only(top: 24.0),
children: [ children: listViewChildren,
if (messages.isEmpty && !showIndicator) ...[
_ChatNoMessages(widget: widget),
],
for (var (index, message) in messages.indexed) ...[
if (widget.chat.id == message.chatId) ...[
_ChatBubble(
key: ValueKey(message.id),
message: message,
previousMessage: index < messages.length - 1
? messages[index + 1]
: null,
onPressUserProfile: widget.onPressUserProfile,
),
],
],
],
),
);
},
), ),
), ),
), ),
_ChatBottom( _ChatBottom(
chat: widget.chat, chat: chat!,
onPressSelectImage: () async => onPressSelectImage.call( onPressSelectImage: () async => onPressSelectImage(
context, context,
options, options,
widget.onUploadImage, onUploadImage,
), ),
onMessageSubmit: widget.onMessageSubmit, onMessageSubmit: onMessageSubmit,
), ),
], ],
), ),
if (showIndicator && options.enableLoadingIndicator) ...[ if (showIndicator.value && options.enableLoadingIndicator)
options.builders.loadingWidgetBuilder.call(context) ?? options.builders.loadingWidgetBuilder(context) ??
const SizedBox.shrink(), const SizedBox.shrink(),
], ],
],
); );
} }
} }
class _ChatNoMessages extends StatelessWidget { /// Widget displayed when there are no messages in the chat.
class _ChatNoMessages extends HookWidget {
const _ChatNoMessages({ const _ChatNoMessages({
required this.widget, required this.isGroupChat,
}); });
final _Body widget; /// Determines if this chat is a group chat.
final bool isGroupChat;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var chatScope = ChatScope.of(context); var chatScope = ChatScope.of(context);
var options = chatScope.options; var translations = chatScope.options.translations;
var translations = options.translations;
var theme = Theme.of(context); var theme = Theme.of(context);
return Center( return Center(
child: Text( child: Text(
widget.chat.isGroupChat isGroupChat
? translations.writeFirstMessageInGroupChat ? translations.writeFirstMessageInGroupChat
: translations.writeMessageToStartChat, : translations.writeMessageToStartChat,
style: theme.textTheme.bodySmall, style: theme.textTheme.bodySmall,
@ -341,69 +339,64 @@ class _ChatNoMessages extends StatelessWidget {
} }
} }
class _ChatBottom extends StatefulWidget { /// Bottom input field where the user can type or upload images.
class _ChatBottom extends HookWidget {
const _ChatBottom({ const _ChatBottom({
required this.chat, required this.chat,
required this.onMessageSubmit, required this.onMessageSubmit,
this.onPressSelectImage, this.onPressSelectImage,
}); });
/// The chat model.
final ChatModel chat;
/// Callback function invoked when a message is submitted. /// Callback function invoked when a message is submitted.
final Function(String text) onMessageSubmit; final Function(String text) onMessageSubmit;
/// Callback function invoked when the select image button is pressed. /// Callback function invoked when the select image button is pressed.
final VoidCallback? onPressSelectImage; final VoidCallback? onPressSelectImage;
/// The chat model.
final ChatModel chat;
@override
State<_ChatBottom> createState() => _ChatBottomState();
}
class _ChatBottomState extends State<_ChatBottom> {
final TextEditingController _textEditingController = TextEditingController();
bool _isTyping = false;
bool _isSending = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var chatScope = ChatScope.of(context); var chatScope = ChatScope.of(context);
var options = chatScope.options; var options = chatScope.options;
var theme = Theme.of(context); var theme = Theme.of(context);
_textEditingController.addListener(() { var textController = useTextEditingController();
setState(() { var isTyping = useState(false);
_isTyping = _textEditingController.text.isNotEmpty; var isSending = useState(false);
});
}); useEffect(
() {
void listener() => isTyping.value = textController.text.isNotEmpty;
textController.addListener(listener);
return () => textController.removeListener(listener);
},
[textController],
);
Future<void> sendMessage() async { Future<void> sendMessage() async {
setState(() { isSending.value = true;
_isSending = true; var value = textController.text;
});
var value = _textEditingController.text;
if (value.isNotEmpty) { if (value.isNotEmpty) {
await widget.onMessageSubmit(value); await onMessageSubmit(value);
_textEditingController.clear(); textController.clear();
} }
setState(() { isSending.value = false;
_isSending = false;
});
} }
Future<void> Function()? onClickSendMessage; Future<void> Function()? onClickSendMessage;
if (_isTyping && !_isSending) { if (isTyping.value && !isSending.value) {
onClickSendMessage = () async => sendMessage(); onClickSendMessage = () async => sendMessage();
} }
/// Image and send buttons
var messageSendButtons = Row( var messageSendButtons = Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( IconButton(
onPressed: widget.onPressSelectImage, onPressed: onPressSelectImage,
icon: Icon( icon: Icon(
Icons.image_outlined, Icons.image_outlined,
color: options.iconEnabledColor, color: options.iconEnabledColor,
@ -413,9 +406,7 @@ class _ChatBottomState extends State<_ChatBottom> {
disabledColor: options.iconDisabledColor, disabledColor: options.iconDisabledColor,
color: options.iconEnabledColor, color: options.iconEnabledColor,
onPressed: onClickSendMessage, onPressed: onClickSendMessage,
icon: const Icon( icon: const Icon(Icons.send_rounded),
Icons.send_rounded,
),
), ),
], ],
); );
@ -425,19 +416,15 @@ class _ChatBottomState extends State<_ChatBottom> {
textAlignVertical: TextAlignVertical.center, textAlignVertical: TextAlignVertical.center,
style: theme.textTheme.bodySmall, style: theme.textTheme.bodySmall,
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
controller: _textEditingController, controller: textController,
decoration: InputDecoration( decoration: InputDecoration(
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
borderSide: const BorderSide( borderSide: const BorderSide(color: Colors.black),
color: Colors.black,
),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
borderSide: const BorderSide( borderSide: const BorderSide(color: Colors.black),
color: Colors.black,
),
), ),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
vertical: 0, vertical: 0,
@ -448,9 +435,7 @@ class _ChatBottomState extends State<_ChatBottom> {
fillColor: Colors.white, fillColor: Colors.white,
filled: true, filled: true,
border: const OutlineInputBorder( border: const OutlineInputBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(Radius.circular(25)),
Radius.circular(25),
),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
suffixIcon: messageSendButtons, suffixIcon: messageSendButtons,
@ -458,15 +443,12 @@ class _ChatBottomState extends State<_ChatBottom> {
); );
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
horizontal: 12,
vertical: 16,
),
child: SizedBox( child: SizedBox(
height: 45, height: 45,
child: options.builders.messageInputBuilder?.call( child: options.builders.messageInputBuilder?.call(
context, context,
_textEditingController, textController,
messageSendButtons, messageSendButtons,
options.translations, options.translations,
) ?? ) ??
@ -476,52 +458,57 @@ class _ChatBottomState extends State<_ChatBottom> {
} }
} }
class _ChatBubble extends StatefulWidget { /// A single chat bubble in the chat
class _ChatBubble extends HookWidget {
const _ChatBubble({ const _ChatBubble({
required this.message, required this.message,
required this.onPressUserProfile, required this.onPressUserProfile,
this.previousMessage, this.previousMessage,
super.key, super.key,
}); });
/// The message to display.
final MessageModel message; final MessageModel message;
/// The previous message in the list, if any.
final MessageModel? previousMessage; final MessageModel? previousMessage;
/// Callback function when the user's profile is pressed.
final Function(UserModel user) onPressUserProfile; final Function(UserModel user) onPressUserProfile;
@override
State<_ChatBubble> createState() => _ChatBubbleState();
}
class _ChatBubbleState extends State<_ChatBubble> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var chatScope = ChatScope.of(context); var chatScope = ChatScope.of(context);
var options = chatScope.options;
var service = chatScope.service; var service = chatScope.service;
return StreamBuilder<UserModel>( var options = chatScope.options;
stream: service.getUser(userId: widget.message.senderId),
builder: (context, snapshot) { var userStream = useMemoized(
if (snapshot.connectionState == ConnectionState.waiting) { () => service.getUser(userId: message.senderId),
return const Center( [message.senderId],
child: CircularProgressIndicator(),
); );
var userSnapshot = useStream(userStream);
if (userSnapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} }
var user = snapshot.data!; var user = userSnapshot.data;
if (user == null) {
return const SizedBox.shrink();
}
return options.builders.chatMessageBuilder.call( return options.builders.chatMessageBuilder.call(
context, context,
widget.message, message,
widget.previousMessage, previousMessage,
user, user,
widget.onPressUserProfile, onPressUserProfile,
) ?? ) ??
DefaultChatMessageBuilder( DefaultChatMessageBuilder(
message: widget.message, message: message,
previousMessage: widget.previousMessage, previousMessage: previousMessage,
user: user, user: user,
onPressUserProfile: widget.onPressUserProfile, onPressUserProfile: onPressUserProfile,
);
},
); );
} }
} }