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
This commit is contained in:
Freek van de Ven 2025-01-30 10:38:34 +01:00 committed by FlutterJoey
parent 1ea2887e27
commit 7457602afe
3 changed files with 202 additions and 225 deletions

View file

@ -1,5 +1,6 @@
## 5.0.0 - WIP ## 5.0.0 - WIP
- Get the color for the imagepicker from the Theme's primaryColor - 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 ## 4.0.0
- Move to the new user story architecture - Move to the new user story architecture

View file

@ -17,6 +17,7 @@ class ChatBuilders {
this.newChatButtonBuilder, this.newChatButtonBuilder,
this.noUsersPlaceholderBuilder, this.noUsersPlaceholderBuilder,
this.chatTitleBuilder, this.chatTitleBuilder,
this.chatMessageBuilder,
this.usernameBuilder, this.usernameBuilder,
this.loadingWidgetBuilder, this.loadingWidgetBuilder,
}); });
@ -62,6 +63,9 @@ class ChatBuilders {
/// The chat title builder /// The chat title builder
final Widget Function(String chatTitle)? chatTitleBuilder; final Widget Function(String chatTitle)? chatTitleBuilder;
/// The chat message builder
final ChatMessageBuilder? chatMessageBuilder;
/// The username builder /// The username builder
final Widget Function(String userFullName)? usernameBuilder; final Widget Function(String userFullName)? usernameBuilder;
@ -108,6 +112,17 @@ typedef ContainerBuilder = Widget Function(
Widget child, 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 /// The group avatar builder
typedef GroupAvatarBuilder = Widget Function( typedef GroupAvatarBuilder = Widget Function(
BuildContext context, BuildContext context,

View file

@ -86,46 +86,36 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
@override @override
Widget build(BuildContext context) { 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) { if (widget.chatOptions.builders.baseScreenBuilder == null) {
return Scaffold( return Scaffold(
appBar: _AppBar( appBar: appBar,
chatTitle: chatTitle, body: body,
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,
),
); );
} }
return widget.chatOptions.builders.baseScreenBuilder!.call( return widget.chatOptions.builders.baseScreenBuilder!.call(
context, context,
widget.mapScreenType, widget.mapScreenType,
_AppBar( appBar,
chatTitle: chatTitle, body,
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,
),
); );
} }
} }
@ -375,16 +365,53 @@ class _ChatBottomState extends State<_ChatBottom> {
var theme = Theme.of(context); var theme = Theme.of(context);
_textEditingController.addListener(() { _textEditingController.addListener(() {
if (_textEditingController.text.isEmpty) { setState(() {
setState(() { _isTyping = _textEditingController.text.isNotEmpty;
_isTyping = false; });
});
} else {
setState(() {
_isTyping = true;
});
}
}); });
Future<void> sendMessage() async {
setState(() {
_isSending = true;
});
var value = _textEditingController.text;
if (value.isNotEmpty) {
await widget.onMessageSubmit(value);
_textEditingController.clear();
}
setState(() {
_isSending = false;
});
}
Future<void> 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( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
@ -395,44 +422,7 @@ class _ChatBottomState extends State<_ChatBottom> {
child: widget.options.builders.messageInputBuilder?.call( child: widget.options.builders.messageInputBuilder?.call(
context, context,
_textEditingController, _textEditingController,
Row( messageSendButtons,
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,
),
),
],
),
widget.options.translations, widget.options.translations,
) ?? ) ??
TextField( TextField(
@ -468,43 +458,7 @@ class _ChatBottomState extends State<_ChatBottom> {
), ),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
suffixIcon: Row( suffixIcon: messageSendButtons,
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,
),
),
],
),
), ),
), ),
), ),
@ -557,123 +511,130 @@ class _ChatBubbleState extends State<_ChatBubble> {
var user = snapshot.data!; var user = snapshot.data!;
return Padding( return widget.options.builders.chatMessageBuilder?.call(
padding: EdgeInsets.only( context,
top: isNewDate || isSameSender ? 25.0 : 0, widget.message,
), widget.previousMessage,
child: Row( ) ??
crossAxisAlignment: CrossAxisAlignment.start, Padding(
children: [ padding: EdgeInsets.only(
if (isNewDate || isSameSender) ...[ top: isNewDate || isSameSender ? 25.0 : 0,
InkWell( ),
onTap: () => widget.onPressUserProfile(user), child: Row(
child: Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.only(left: 10.0), children: [
child: user.imageUrl?.isNotEmpty ?? false if (isNewDate || isSameSender) ...[
? _ChatImage( InkWell(
image: user.imageUrl!, onTap: () => widget.onPressUserProfile(user),
) child: Padding(
: widget.options.builders.userAvatarBuilder?.call( padding: const EdgeInsets.only(left: 10.0),
context, child: user.imageUrl?.isNotEmpty ?? false
user, ? _ChatImage(
40, image: user.imageUrl!,
) ?? )
Avatar( : widget.options.builders.userAvatarBuilder?.call(
key: ValueKey(user.id), context,
boxfit: BoxFit.cover, user,
user: User( 40,
firstName: user.firstName, ) ??
lastName: user.lastName, Avatar(
imageUrl: key: ValueKey(user.id),
user.imageUrl != "" ? user.imageUrl : null, boxfit: BoxFit.cover,
), user: User(
size: 40, firstName: user.firstName,
), lastName: user.lastName,
), imageUrl: user.imageUrl != ""
), ? user.imageUrl
] else ...[ : null,
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,
), ),
), size: 40,
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Text(
dateFormatter.format(
date: widget.message.timestamp,
showFullDate: true,
), ),
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),
Padding( child: widget.message.isTextMessage
padding: const EdgeInsets.only(top: 3.0), ? Row(
child: widget.message.isTextMessage crossAxisAlignment: CrossAxisAlignment.end,
? Row( mainAxisAlignment:
crossAxisAlignment: CrossAxisAlignment.end, MainAxisAlignment.spaceBetween,
mainAxisAlignment: children: [
MainAxisAlignment.spaceBetween, Flexible(
children: [ child: Text(
Flexible( widget.message.text ?? "",
child: Text( style: theme.textTheme.bodySmall,
widget.message.text ?? "", ),
style: theme.textTheme.bodySmall, ),
), if (widget.options.showTimes &&
), !isSameMinute &&
if (widget.options.showTimes && !isNewDate &&
!isSameMinute && !hasHeader)
!isNewDate && Text(
!hasHeader) dateFormatter
Text( .format(
dateFormatter date: widget.message.timestamp,
.format( showFullDate: true,
date: widget.message.timestamp, )
showFullDate: true, .split(" ")
) .last,
.split(" ") style: theme.textTheme.labelSmall,
.last, textAlign: TextAlign.end,
style: theme.textTheme.labelSmall, ),
textAlign: TextAlign.end, ],
),
],
)
: widget.message.isImageMessage
? CachedNetworkImage(
imageUrl: widget.message.imageUrl ?? "",
) )
: const SizedBox.shrink(), : widget.message.isImageMessage
? CachedNetworkImage(
imageUrl: widget.message.imageUrl ?? "",
)
: const SizedBox.shrink(),
),
],
), ),
], ),
), ),
), ],
), ),
], );
),
);
}, },
); );
} }