From 7457602afebe2326b89d23051f481a4dee9a057e Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Thu, 30 Jan 2025 10:38:34 +0100 Subject: [PATCH] feat: add chatMessageBuilder to the chatoptions to override default behavior With the chatMessageBuilder it is possible to run a null whenever you still want to use the default but only want to update the chat in very specific cases. I also slightly refactored the chat_detail_screen.dart to remove duplicate code and make it more readable --- CHANGELOG.md | 1 + .../lib/src/config/chat_builders.dart | 15 + .../lib/src/screens/chat_detail_screen.dart | 411 ++++++++---------- 3 files changed, 202 insertions(+), 225 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fdb2c7..6444782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 5.0.0 - WIP - Get the color for the imagepicker from the Theme's primaryColor +- Added chatMessageBuilder to the userstory configuration to customize the chat messages ## 4.0.0 - Move to the new user story architecture diff --git a/packages/flutter_chat/lib/src/config/chat_builders.dart b/packages/flutter_chat/lib/src/config/chat_builders.dart index 921b096..c286d07 100644 --- a/packages/flutter_chat/lib/src/config/chat_builders.dart +++ b/packages/flutter_chat/lib/src/config/chat_builders.dart @@ -17,6 +17,7 @@ class ChatBuilders { this.newChatButtonBuilder, this.noUsersPlaceholderBuilder, this.chatTitleBuilder, + this.chatMessageBuilder, this.usernameBuilder, this.loadingWidgetBuilder, }); @@ -62,6 +63,9 @@ class ChatBuilders { /// The chat title builder final Widget Function(String chatTitle)? chatTitleBuilder; + /// The chat message builder + final ChatMessageBuilder? chatMessageBuilder; + /// The username builder final Widget Function(String userFullName)? usernameBuilder; @@ -108,6 +112,17 @@ typedef ContainerBuilder = Widget Function( Widget child, ); +/// The chat message builder +/// This builder is used to override the default chat message widget +/// If null is returned, the default chat message widget will be used so you can +/// override for specific cases +/// [previousMessage] is the previous message in the chat +typedef ChatMessageBuilder = Widget? Function( + BuildContext context, + MessageModel message, + MessageModel? previousMessage, +); + /// The group avatar builder typedef GroupAvatarBuilder = Widget Function( BuildContext context, diff --git a/packages/flutter_chat/lib/src/screens/chat_detail_screen.dart b/packages/flutter_chat/lib/src/screens/chat_detail_screen.dart index d27d3f9..332eb58 100644 --- a/packages/flutter_chat/lib/src/screens/chat_detail_screen.dart +++ b/packages/flutter_chat/lib/src/screens/chat_detail_screen.dart @@ -86,46 +86,36 @@ class _ChatDetailScreenState extends State { @override Widget build(BuildContext context) { + var appBar = _AppBar( + chatTitle: chatTitle, + chatOptions: widget.chatOptions, + onPressChatTitle: widget.onPressChatTitle, + chatModel: widget.chat, + ); + + var body = _Body( + chatService: widget.chatService, + options: widget.chatOptions, + chat: widget.chat, + currentUserId: widget.userId, + onPressUserProfile: widget.onPressUserProfile, + onUploadImage: widget.onUploadImage, + onMessageSubmit: widget.onMessageSubmit, + onReadChat: widget.onReadChat, + ); + if (widget.chatOptions.builders.baseScreenBuilder == null) { return Scaffold( - appBar: _AppBar( - chatTitle: chatTitle, - chatOptions: widget.chatOptions, - onPressChatTitle: widget.onPressChatTitle, - chatModel: widget.chat, - ), - body: _Body( - chatService: widget.chatService, - options: widget.chatOptions, - chat: widget.chat, - currentUserId: widget.userId, - onPressUserProfile: widget.onPressUserProfile, - onUploadImage: widget.onUploadImage, - onMessageSubmit: widget.onMessageSubmit, - onReadChat: widget.onReadChat, - ), + appBar: appBar, + body: body, ); } return widget.chatOptions.builders.baseScreenBuilder!.call( context, widget.mapScreenType, - _AppBar( - chatTitle: chatTitle, - chatOptions: widget.chatOptions, - onPressChatTitle: widget.onPressChatTitle, - chatModel: widget.chat, - ), - _Body( - chatService: widget.chatService, - options: widget.chatOptions, - chat: widget.chat, - currentUserId: widget.userId, - onPressUserProfile: widget.onPressUserProfile, - onUploadImage: widget.onUploadImage, - onMessageSubmit: widget.onMessageSubmit, - onReadChat: widget.onReadChat, - ), + appBar, + body, ); } } @@ -375,16 +365,53 @@ class _ChatBottomState extends State<_ChatBottom> { var theme = Theme.of(context); _textEditingController.addListener(() { - if (_textEditingController.text.isEmpty) { - setState(() { - _isTyping = false; - }); - } else { - setState(() { - _isTyping = true; - }); - } + setState(() { + _isTyping = _textEditingController.text.isNotEmpty; + }); }); + + Future sendMessage() async { + setState(() { + _isSending = true; + }); + + var value = _textEditingController.text; + if (value.isNotEmpty) { + await widget.onMessageSubmit(value); + _textEditingController.clear(); + } + setState(() { + _isSending = false; + }); + } + + Future Function()? onClickSendMessage; + if (_isTyping && !_isSending) { + onClickSendMessage = () async => sendMessage(); + } + + var messageSendButtons = Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: widget.onPressSelectImage, + icon: Icon( + Icons.image_outlined, + color: widget.options.iconEnabledColor, + ), + ), + IconButton( + disabledColor: widget.options.iconDisabledColor, + color: widget.options.iconEnabledColor, + onPressed: onClickSendMessage, + icon: const Icon( + Icons.send, + ), + ), + ], + ); + return Padding( padding: const EdgeInsets.symmetric( horizontal: 12, @@ -395,44 +422,7 @@ class _ChatBottomState extends State<_ChatBottom> { child: widget.options.builders.messageInputBuilder?.call( context, _textEditingController, - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: widget.onPressSelectImage, - icon: Icon( - Icons.image_outlined, - color: widget.options.iconEnabledColor, - ), - ), - IconButton( - disabledColor: widget.options.iconDisabledColor, - color: widget.options.iconEnabledColor, - onPressed: _isTyping && !_isSending - ? () async { - setState(() { - _isSending = true; - }); - - var value = _textEditingController.text; - - if (value.isNotEmpty) { - await widget.onMessageSubmit(value); - _textEditingController.clear(); - } - - setState(() { - _isSending = false; - }); - } - : null, - icon: const Icon( - Icons.send, - ), - ), - ], - ), + messageSendButtons, widget.options.translations, ) ?? TextField( @@ -468,43 +458,7 @@ class _ChatBottomState extends State<_ChatBottom> { ), borderSide: BorderSide.none, ), - suffixIcon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: widget.onPressSelectImage, - icon: Icon( - Icons.image_outlined, - color: widget.options.iconEnabledColor, - ), - ), - IconButton( - disabledColor: widget.options.iconDisabledColor, - color: widget.options.iconEnabledColor, - onPressed: _isTyping && !_isSending - ? () async { - setState(() { - _isSending = true; - }); - - var value = _textEditingController.text; - - if (value.isNotEmpty) { - await widget.onMessageSubmit(value); - _textEditingController.clear(); - } - - setState(() { - _isSending = false; - }); - } - : null, - icon: const Icon( - Icons.send, - ), - ), - ], - ), + suffixIcon: messageSendButtons, ), ), ), @@ -557,123 +511,130 @@ class _ChatBubbleState extends State<_ChatBubble> { var user = snapshot.data!; - return Padding( - padding: EdgeInsets.only( - top: isNewDate || isSameSender ? 25.0 : 0, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isNewDate || isSameSender) ...[ - InkWell( - onTap: () => widget.onPressUserProfile(user), - child: Padding( - padding: const EdgeInsets.only(left: 10.0), - child: user.imageUrl?.isNotEmpty ?? false - ? _ChatImage( - image: user.imageUrl!, - ) - : widget.options.builders.userAvatarBuilder?.call( - context, - user, - 40, - ) ?? - Avatar( - key: ValueKey(user.id), - boxfit: BoxFit.cover, - user: User( - firstName: user.firstName, - lastName: user.lastName, - imageUrl: - user.imageUrl != "" ? user.imageUrl : null, - ), - size: 40, - ), - ), - ), - ] else ...[ - const SizedBox( - width: 50, - ), - ], - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 22.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (isNewDate || isSameSender) ...[ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: widget.options.builders.usernameBuilder - ?.call( - user.fullname ?? "", - ) ?? - Text( - user.fullname ?? translations.anonymousUser, - style: theme.textTheme.titleMedium, + return widget.options.builders.chatMessageBuilder?.call( + context, + widget.message, + widget.previousMessage, + ) ?? + Padding( + padding: EdgeInsets.only( + top: isNewDate || isSameSender ? 25.0 : 0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isNewDate || isSameSender) ...[ + InkWell( + onTap: () => widget.onPressUserProfile(user), + child: Padding( + padding: const EdgeInsets.only(left: 10.0), + child: user.imageUrl?.isNotEmpty ?? false + ? _ChatImage( + image: user.imageUrl!, + ) + : widget.options.builders.userAvatarBuilder?.call( + context, + user, + 40, + ) ?? + Avatar( + key: ValueKey(user.id), + boxfit: BoxFit.cover, + user: User( + firstName: user.firstName, + lastName: user.lastName, + imageUrl: user.imageUrl != "" + ? user.imageUrl + : null, ), - ), - Padding( - padding: const EdgeInsets.only(top: 5.0), - child: Text( - dateFormatter.format( - date: widget.message.timestamp, - showFullDate: true, + size: 40, ), - style: theme.textTheme.labelSmall, - ), + ), + ), + ] else ...[ + const SizedBox( + width: 50, + ), + ], + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 22.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (isNewDate || isSameSender) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: widget.options.builders.usernameBuilder + ?.call( + user.fullname ?? "", + ) ?? + Text( + user.fullname ?? + translations.anonymousUser, + style: theme.textTheme.titleMedium, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 5.0), + child: Text( + dateFormatter.format( + date: widget.message.timestamp, + showFullDate: true, + ), + style: theme.textTheme.labelSmall, + ), + ), + ], ), ], - ), - ], - Padding( - padding: const EdgeInsets.only(top: 3.0), - child: widget.message.isTextMessage - ? Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - widget.message.text ?? "", - style: theme.textTheme.bodySmall, - ), - ), - if (widget.options.showTimes && - !isSameMinute && - !isNewDate && - !hasHeader) - Text( - dateFormatter - .format( - date: widget.message.timestamp, - showFullDate: true, - ) - .split(" ") - .last, - style: theme.textTheme.labelSmall, - textAlign: TextAlign.end, - ), - ], - ) - : widget.message.isImageMessage - ? CachedNetworkImage( - imageUrl: widget.message.imageUrl ?? "", + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: widget.message.isTextMessage + ? Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + widget.message.text ?? "", + style: theme.textTheme.bodySmall, + ), + ), + if (widget.options.showTimes && + !isSameMinute && + !isNewDate && + !hasHeader) + Text( + dateFormatter + .format( + date: widget.message.timestamp, + showFullDate: true, + ) + .split(" ") + .last, + style: theme.textTheme.labelSmall, + textAlign: TextAlign.end, + ), + ], ) - : const SizedBox.shrink(), + : widget.message.isImageMessage + ? CachedNetworkImage( + imageUrl: widget.message.imageUrl ?? "", + ) + : const SizedBox.shrink(), + ), + ], ), - ], + ), ), - ), + ], ), - ], - ), - ); + ); }, ); }