Merge pull request #497 from GetStream/feat/defaultMessageWidget

feat: make default message widget easier to reuse
This commit is contained in:
Salvatore Giordano
2021-06-25 15:53:23 +02:00
committed by GitHub
7 changed files with 252 additions and 295 deletions
@@ -93,6 +93,7 @@ class ChannelPage extends StatelessWidget {
BuildContext context, BuildContext context,
MessageDetails details, MessageDetails details,
List<Message> messages, List<Message> messages,
MessageWidget _,
) { ) {
final message = details.message; final message = details.message;
final isCurrentUser = StreamChat.of(context).user!.id == message.user!.id; final isCurrentUser = StreamChat.of(context).user!.id == message.user!.id;
@@ -15,6 +15,7 @@ class MessageActionsModal extends StatefulWidget {
const MessageActionsModal({ const MessageActionsModal({
Key? key, Key? key,
required this.message, required this.message,
required this.messageWidget,
required this.messageTheme, required this.messageTheme,
this.showReactions = true, this.showReactions = true,
this.showDeleteMessage = true, this.showDeleteMessage = true,
@@ -27,18 +28,15 @@ class MessageActionsModal extends StatefulWidget {
this.showThreadReplyMessage = true, this.showThreadReplyMessage = true,
this.showFlagButton = true, this.showFlagButton = true,
this.showPinButton = true, this.showPinButton = true,
this.showPinHighlight = false,
this.showUserAvatar = DisplayWidget.show,
this.editMessageInputBuilder, this.editMessageInputBuilder,
this.messageShape,
this.attachmentShape,
this.reverse = false, this.reverse = false,
this.customActions = const [], this.customActions = const [],
this.attachmentBorderRadiusGeometry,
this.onCopyTap, this.onCopyTap,
this.textBuilder,
}) : super(key: key); }) : super(key: key);
/// Widget that shows the message
final Widget messageWidget;
/// Builder for edit message /// Builder for edit message
final Widget Function(BuildContext, Message)? editMessageInputBuilder; final Widget Function(BuildContext, Message)? editMessageInputBuilder;
@@ -84,30 +82,12 @@ class MessageActionsModal extends StatefulWidget {
/// Flag for showing pin action /// Flag for showing pin action
final bool showPinButton; final bool showPinButton;
/// Display Pin Highlight
final bool showPinHighlight;
/// Flag for reversing message /// Flag for reversing message
final bool reverse; final bool reverse;
/// [ShapeBorder] to apply to the widget
final ShapeBorder? messageShape;
/// [ShapeBorder] to apply to attachment
final ShapeBorder? attachmentShape;
/// Enum for displaying user avatar
final DisplayWidget showUserAvatar;
/// [BorderRadius] for attachment border
final BorderRadius? attachmentBorderRadiusGeometry;
/// List of custom actions /// List of custom actions
final List<MessageAction> customActions; final List<MessageAction> customActions;
/// Customize the MessageWidget textBuilder
final Widget Function(BuildContext context, Message message)? textBuilder;
@override @override
_MessageActionsModalState createState() => _MessageActionsModalState(); _MessageActionsModalState createState() => _MessageActionsModalState();
} }
@@ -142,9 +122,6 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
? 1 ? 1
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
final hasFileAttachment =
widget.message.attachments.any((it) => it.type == 'file') == true;
final streamChatThemeData = StreamChatTheme.of(context); final streamChatThemeData = StreamChatTheme.of(context);
final numberOfReactions = streamChatThemeData.reactionIcons.length; final numberOfReactions = streamChatThemeData.reactionIcons.length;
@@ -178,41 +155,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
IgnorePointer( IgnorePointer(
child: MessageWidget( child: widget.messageWidget,
key: const Key('MessageWidget'),
reverse: widget.reverse,
attachmentBorderRadiusGeometry: widget
.attachmentBorderRadiusGeometry
?.mirrorBorderIfReversed(reverse: !widget.reverse),
message: widget.message.copyWith(
text: widget.message.text!.length > 200
// ignore: lines_longer_than_80_chars
? '${widget.message.text!.substring(0, 200)}...'
: widget.message.text,
),
messageTheme: widget.messageTheme,
showReactions: false,
showUsername: false,
showReplyMessage: false,
showUserAvatar: widget.showUserAvatar,
attachmentPadding: EdgeInsets.all(
hasFileAttachment ? 4 : 2,
),
showTimestamp: false,
translateUserAvatar: false,
padding: const EdgeInsets.all(0),
textPadding: EdgeInsets.symmetric(
vertical: 8,
horizontal: widget.message.text!.isOnlyEmoji ? 0 : 16.0,
),
showReactionPickerIndicator: widget.showReactions &&
(widget.message.status == MessageSendingStatus.sent),
showSendingIndicator: false,
shape: widget.messageShape,
attachmentShape: widget.attachmentShape,
showPinHighlight: false,
textBuilder: widget.textBuilder,
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Padding( Padding(
@@ -19,16 +19,22 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
/// Widget builder for message /// Widget builder for message
/// [defaultMessageWidget] is the default [MessageWidget] configuration
/// Use [defaultMessageWidget.copyWith] to easily customize it
typedef MessageBuilder = Widget Function( typedef MessageBuilder = Widget Function(
BuildContext, BuildContext,
MessageDetails, MessageDetails,
List<Message>, List<Message>,
MessageWidget defaultMessageWidget,
); );
/// Widget builder for parent message /// Widget builder for parent message
/// [defaultMessageWidget] is the default [MessageWidget] configuration
/// Use [defaultMessageWidget.copyWith] to easily customize it
typedef ParentMessageBuilder = Widget Function( typedef ParentMessageBuilder = Widget Function(
BuildContext, BuildContext,
Message?, Message?,
MessageWidget defaultMessageWidget,
); );
/// Widget builder for system message /// Widget builder for system message
@@ -56,12 +62,12 @@ typedef ReplyTapCallback = void Function(Message);
class MessageDetails { class MessageDetails {
/// Constructor for creating [MessageDetails] /// Constructor for creating [MessageDetails]
MessageDetails( MessageDetails(
BuildContext context, String currentUserId,
this.message, this.message,
List<Message> messages, List<Message> messages,
this.index, this.index,
) { ) {
isMyMessage = message.user?.id == StreamChat.of(context).user?.id; isMyMessage = message.user?.id == currentUserId;
isLastUser = index + 1 < messages.length && isLastUser = index + 1 < messages.length &&
message.user?.id == messages[index + 1].user?.id; message.user?.id == messages[index + 1].user?.id;
isNextUser = isNextUser =
@@ -69,19 +75,19 @@ class MessageDetails {
} }
/// True if the message belongs to the current user /// True if the message belongs to the current user
bool? isMyMessage; late final bool isMyMessage;
/// True if the user message is the same of the previous message /// True if the user message is the same of the previous message
bool? isLastUser; late final bool isLastUser;
/// True if the user message is the same of the next message /// True if the user message is the same of the next message
bool? isNextUser; late final bool isNextUser;
/// The message /// The message
Message message; final Message message;
/// The index of the message /// The index of the message
int index; final int index;
} }
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png)
@@ -545,11 +551,7 @@ class _MessageListViewState extends State<MessageListView> {
itemBuilder: (context, i) { itemBuilder: (context, i) {
if (i == itemCount - 1) { if (i == itemCount - 1) {
if (widget.parentMessage == null) return const Offstage(); if (widget.parentMessage == null) return const Offstage();
return widget.parentMessageBuilder?.call( return buildParentMessage(widget.parentMessage!);
context,
widget.parentMessage,
) ??
buildParentMessage(widget.parentMessage!);
} }
if (i == itemCount - 2) { if (i == itemCount - 2) {
@@ -576,46 +578,21 @@ class _MessageListViewState extends State<MessageListView> {
const Offstage(); const Offstage();
} }
final topMessageIndex = itemCount -
4; // 7 -> parent // 6 -> header // 5 -> loader
const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer
final message = messages[i - 2]; final message = messages[i - 2];
Widget messageWidget; Widget messageWidget;
if (i == topMessageIndex) { if (i == bottomMessageIndex) {
messageWidget = _buildTopMessage(
context,
message,
messages,
streamChannel,
i,
);
} else if (i == bottomMessageIndex) {
messageWidget = _buildBottomMessage( messageWidget = _buildBottomMessage(
context, context,
message, message,
messages, messages,
streamChannel!, streamChannel!,
i, i - 2,
); );
} else { } else {
if (widget.messageBuilder != null) { messageWidget = buildMessage(message, messages, i - 2);
messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'),
builder: (context) => widget.messageBuilder!(
context,
MessageDetails(
context,
message,
messages,
i,
),
messages),
);
} else {
messageWidget = buildMessage(message, messages, i);
}
} }
return messageWidget; return messageWidget;
}, },
@@ -785,34 +762,6 @@ class _MessageListViewState extends State<MessageListView> {
isThreadConversation: _isThreadConversation, isThreadConversation: _isThreadConversation,
); );
Widget _buildTopMessage(
BuildContext context,
Message message,
List<Message> messages,
StreamChannelState? streamChannel,
int index,
) {
Widget messageWidget;
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: const ValueKey<String>('TOP-MESSAGE'),
builder: (_) => widget.messageBuilder!(
context,
MessageDetails(
context,
message,
messages,
messages.length - 1,
),
messages,
),
);
} else {
messageWidget = buildMessage(message, messages, index);
}
return messageWidget;
}
Widget _buildBottomMessage( Widget _buildBottomMessage(
BuildContext context, BuildContext context,
Message message, Message message,
@@ -820,24 +769,7 @@ class _MessageListViewState extends State<MessageListView> {
StreamChannelState streamChannel, StreamChannelState streamChannel,
int index, int index,
) { ) {
Widget messageWidget; final messageWidget = buildMessage(message, messages, index);
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey<String>('BOTTOM-MESSAGE-${message.id}'),
builder: (_) => widget.messageBuilder!(
context,
MessageDetails(
context,
message,
messages,
0,
),
messages,
),
);
} else {
messageWidget = buildMessage(message, messages, index);
}
return VisibilityDetector( return VisibilityDetector(
key: ValueKey<String>('BOTTOM-MESSAGE-${message.id}'), key: ValueKey<String>('BOTTOM-MESSAGE-${message.id}'),
@@ -871,7 +803,7 @@ class _MessageListViewState extends State<MessageListView> {
final currentUserMember = final currentUserMember =
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
return MessageWidget( final defaultMessageWidget = MessageWidget(
showReplyMessage: false, showReplyMessage: false,
showResendMessage: false, showResendMessage: false,
showThreadReplyMessage: false, showThreadReplyMessage: false,
@@ -925,6 +857,16 @@ class _MessageListViewState extends State<MessageListView> {
showPinButton: currentUserMember != null && showPinButton: currentUserMember != null &&
widget.pinPermissions.contains(currentUserMember.role), widget.pinPermissions.contains(currentUserMember.role),
); );
if (widget.parentMessageBuilder != null) {
return widget.parentMessageBuilder!.call(
context,
widget.parentMessage,
defaultMessageWidget,
);
}
return defaultMessageWidget;
} }
Widget buildMessage( Widget buildMessage(
@@ -1015,7 +957,7 @@ class _MessageListViewState extends State<MessageListView> {
final currentUserMember = final currentUserMember =
members.firstWhere((e) => e.user!.id == currentUser!.id); members.firstWhere((e) => e.user!.id == currentUser!.id);
Widget child = MessageWidget( Widget messageWidget = MessageWidget(
key: ValueKey<String>('MESSAGE-${message.id}'), key: ValueKey<String>('MESSAGE-${message.id}'),
message: message, message: message,
reverse: isMyMessage, reverse: isMyMessage,
@@ -1130,6 +1072,21 @@ class _MessageListViewState extends State<MessageListView> {
showPinButton: widget.pinPermissions.contains(currentUserMember.role), showPinButton: widget.pinPermissions.contains(currentUserMember.role),
); );
if (widget.messageBuilder != null) {
messageWidget = widget.messageBuilder!(
context,
MessageDetails(
userId,
message,
messages,
index,
),
messages,
messageWidget as MessageWidget,
);
}
var child = messageWidget;
if (!message.isDeleted && if (!message.isDeleted &&
!message.isSystem && !message.isSystem &&
!message.isEphemeral && !message.isEphemeral &&
@@ -1,7 +1,6 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart';
import 'package:stream_chat_flutter/src/reaction_picker.dart'; import 'package:stream_chat_flutter/src/reaction_picker.dart';
import 'package:stream_chat_flutter/src/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_chat.dart';
@@ -15,17 +14,16 @@ class MessageReactionsModal extends StatelessWidget {
const MessageReactionsModal({ const MessageReactionsModal({
Key? key, Key? key,
required this.message, required this.message,
required this.messageWidget,
required this.messageTheme, required this.messageTheme,
this.showReactions = true, this.showReactions = true,
this.messageShape,
this.attachmentShape,
this.reverse = false, this.reverse = false,
this.showUserAvatar = DisplayWidget.show,
this.onUserAvatarTap, this.onUserAvatarTap,
this.attachmentBorderRadiusGeometry,
this.textBuilder,
}) : super(key: key); }) : super(key: key);
/// Widget that shows the message
final Widget messageWidget;
/// Message to display reactions of /// Message to display reactions of
final Message message; final Message message;
@@ -38,24 +36,9 @@ class MessageReactionsModal extends StatelessWidget {
/// Flag to show reactions on message /// Flag to show reactions on message
final bool showReactions; final bool showReactions;
/// Enum to change user avatar config
final DisplayWidget showUserAvatar;
/// [ShapeBorder] to apply to message
final ShapeBorder? messageShape;
/// [ShapeBorder] to apply to attachment
final ShapeBorder? attachmentShape;
/// Callback when user avatar is tapped /// Callback when user avatar is tapped
final void Function(User)? onUserAvatarTap; final void Function(User)? onUserAvatarTap;
/// [BorderRadius] to apply to attachments
final BorderRadius? attachmentBorderRadiusGeometry;
/// Customize the MessageWidget textBuilder
final Widget Function(BuildContext context, Message message)? textBuilder;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
@@ -77,8 +60,6 @@ class MessageReactionsModal extends StatelessWidget {
final divFactor = message.attachments.isNotEmpty == true final divFactor = message.attachments.isNotEmpty == true
? 1 ? 1
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
final hasFileAttachment =
message.attachments.any((it) => it.type == 'file') == true;
final numberOfReactions = StreamChatTheme.of(context).reactionIcons.length; final numberOfReactions = StreamChatTheme.of(context).reactionIcons.length;
final shiftFactor = final shiftFactor =
@@ -110,38 +91,7 @@ class MessageReactionsModal extends StatelessWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
IgnorePointer( IgnorePointer(
child: MessageWidget( child: messageWidget,
key: const Key('MessageWidget'),
reverse: reverse,
message: message.copyWith(
text: message.text!.length > 200
? '${message.text!.substring(0, 200)}...'
: message.text,
),
messageTheme: messageTheme,
showReactions: false,
showUsername: false,
showUserAvatar: showUserAvatar,
showTimestamp: false,
translateUserAvatar: false,
showSendingIndicator: false,
shape: messageShape,
attachmentShape: attachmentShape,
padding: const EdgeInsets.all(0),
attachmentBorderRadiusGeometry: attachmentBorderRadiusGeometry
?.mirrorBorderIfReversed(reverse: !reverse),
attachmentPadding: EdgeInsets.all(
hasFileAttachment ? 4 : 2,
),
textPadding: EdgeInsets.symmetric(
vertical: 8,
horizontal: message.text!.isOnlyEmoji ? 0 : 16.0,
),
showReactionPickerIndicator: showReactions &&
(message.status == MessageSendingStatus.sent),
textBuilder: textBuilder,
showPinHighlight: false,
),
), ),
if (message.latestReactions?.isNotEmpty == true) ...[ if (message.latestReactions?.isNotEmpty == true) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -397,6 +397,117 @@ class MessageWidget extends StatefulWidget {
/// Customize onTap on attachment /// Customize onTap on attachment
final void Function(Message message, Attachment attachment)? onAttachmentTap; final void Function(Message message, Attachment attachment)? onAttachmentTap;
/// Creates a copy of [MessageWidget] with specified attributes overridden.
MessageWidget copyWith({
Key? key,
void Function(User)? onMentionTap,
void Function(Message)? onThreadTap,
void Function(Message)? onReplyTap,
Widget Function(BuildContext, Message)? editMessageInputBuilder,
Widget Function(BuildContext, Message)? textBuilder,
Widget Function(BuildContext, Message)? usernameBuilder,
void Function(BuildContext, Message)? onMessageActions,
Message? message,
MessageTheme? messageTheme,
bool? reverse,
ShapeBorder? shape,
ShapeBorder? attachmentShape,
BorderSide? borderSide,
BorderSide? attachmentBorderSide,
BorderRadiusGeometry? borderRadiusGeometry,
BorderRadiusGeometry? attachmentBorderRadiusGeometry,
EdgeInsetsGeometry? padding,
EdgeInsets? textPadding,
EdgeInsetsGeometry? attachmentPadding,
DisplayWidget? showUserAvatar,
bool? showSendingIndicator,
bool? showReactions,
bool? allRead,
bool? showThreadReplyIndicator,
bool? showInChannelIndicator,
void Function(User)? onUserAvatarTap,
void Function(String)? onLinkTap,
bool? showReactionPickerIndicator,
List<Read>? readList,
ShowMessageCallback? onShowMessage,
ValueChanged<ReturnActionType>? onReturnAction,
bool? showUsername,
bool? showTimestamp,
bool? showReplyMessage,
bool? showThreadReplyMessage,
bool? showEditMessage,
bool? showCopyMessage,
bool? showDeleteMessage,
bool? showResendMessage,
bool? showFlagButton,
bool? showPinButton,
bool? showPinHighlight,
Map<String, AttachmentBuilder>? customAttachmentBuilders,
bool? translateUserAvatar,
OnQuotedMessageTap? onQuotedMessageTap,
void Function(Message)? onMessageTap,
List<MessageAction>? customActions,
void Function(Message message, Attachment attachment)? onAttachmentTap,
}) =>
MessageWidget(
key: key ?? this.key,
onMentionTap: onMentionTap ?? this.onMentionTap,
onThreadTap: onThreadTap ?? this.onThreadTap,
onReplyTap: onReplyTap ?? this.onReplyTap,
editMessageInputBuilder:
editMessageInputBuilder ?? this.editMessageInputBuilder,
textBuilder: textBuilder ?? this.textBuilder,
usernameBuilder: usernameBuilder ?? this.usernameBuilder,
onMessageActions: onMessageActions ?? this.onMessageActions,
message: message ?? this.message,
messageTheme: messageTheme ?? this.messageTheme,
reverse: reverse ?? this.reverse,
shape: shape ?? this.shape,
attachmentShape: attachmentShape ?? this.attachmentShape,
borderSide: borderSide ?? this.borderSide,
attachmentBorderSide: attachmentBorderSide ?? this.attachmentBorderSide,
borderRadiusGeometry: borderRadiusGeometry ?? this.borderRadiusGeometry,
attachmentBorderRadiusGeometry: attachmentBorderRadiusGeometry ??
this.attachmentBorderRadiusGeometry,
padding: padding ?? this.padding,
textPadding: textPadding ?? this.textPadding,
attachmentPadding: attachmentPadding ?? this.attachmentPadding,
showUserAvatar: showUserAvatar ?? this.showUserAvatar,
showSendingIndicator: showSendingIndicator ?? this.showSendingIndicator,
showReactions: showReactions ?? this.showReactions,
allRead: allRead ?? this.allRead,
showThreadReplyIndicator:
showThreadReplyIndicator ?? this.showThreadReplyIndicator,
showInChannelIndicator:
showInChannelIndicator ?? this.showInChannelIndicator,
onUserAvatarTap: onUserAvatarTap ?? this.onUserAvatarTap,
onLinkTap: onLinkTap ?? this.onLinkTap,
showReactionPickerIndicator:
showReactionPickerIndicator ?? this.showReactionPickerIndicator,
readList: readList ?? this.readList,
onShowMessage: onShowMessage ?? this.onShowMessage,
onReturnAction: onReturnAction ?? this.onReturnAction,
showUsername: showUsername ?? this.showUsername,
showTimestamp: showTimestamp ?? this.showTimestamp,
showReplyMessage: showReplyMessage ?? this.showReplyMessage,
showThreadReplyMessage:
showThreadReplyMessage ?? this.showThreadReplyMessage,
showEditMessage: showEditMessage ?? this.showEditMessage,
showCopyMessage: showCopyMessage ?? this.showCopyMessage,
showDeleteMessage: showDeleteMessage ?? this.showDeleteMessage,
showResendMessage: showResendMessage ?? this.showResendMessage,
showFlagButton: showFlagButton ?? this.showFlagButton,
showPinButton: showPinButton ?? this.showPinButton,
showPinHighlight: showPinHighlight ?? this.showPinHighlight,
customAttachmentBuilders:
customAttachmentBuilders ?? attachmentBuilders,
translateUserAvatar: translateUserAvatar ?? this.translateUserAvatar,
onQuotedMessageTap: onQuotedMessageTap ?? this.onQuotedMessageTap,
onMessageTap: onMessageTap ?? this.onMessageTap,
customActions: customActions ?? this.customActions,
onAttachmentTap: onAttachmentTap ?? this.onAttachmentTap,
);
@override @override
_MessageWidgetState createState() => _MessageWidgetState(); _MessageWidgetState createState() => _MessageWidgetState();
} }
@@ -461,7 +572,7 @@ class _MessageWidgetState extends State<MessageWidget>
super.build(context); super.build(context);
final avatarWidth = final avatarWidth =
widget.messageTheme.avatarTheme?.constraints.maxWidth ?? 40; widget.messageTheme.avatarTheme?.constraints.maxWidth ?? 40;
final leftPadding = final bottomRowPadding =
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5; widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
return Material( return Material(
@@ -516,7 +627,8 @@ class _MessageWidgetState extends State<MessageWidget>
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
if (widget.showUserAvatar == if (!widget.reverse &&
widget.showUserAvatar ==
DisplayWidget.show && DisplayWidget.show &&
widget.message.user != null) ...[ widget.message.user != null) ...[
_buildUserAvatar(), _buildUserAvatar(),
@@ -642,6 +754,13 @@ class _MessageWidgetState extends State<MessageWidget>
), ),
), ),
), ),
if (widget.reverse &&
widget.showUserAvatar ==
DisplayWidget.show &&
widget.message.user != null) ...[
_buildUserAvatar(),
const SizedBox(width: 4),
]
], ],
), ),
if (showBottomRow) if (showBottomRow)
@@ -652,7 +771,8 @@ class _MessageWidgetState extends State<MessageWidget>
if (showBottomRow) if (showBottomRow)
Padding( Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: leftPadding, left: !widget.reverse ? bottomRowPadding : 0,
right: widget.reverse ? bottomRowPadding : 0,
bottom: bottom:
isPinned && widget.showPinHighlight ? 6.0 : 0.0, isPinned && widget.showPinHighlight ? 6.0 : 0.0,
), ),
@@ -912,19 +1032,30 @@ class _MessageWidgetState extends State<MessageWidget>
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageActionsModal( child: MessageActionsModal(
textBuilder: widget.textBuilder, messageWidget: widget.copyWith(
key: const Key('MessageWidget'),
message: widget.message.copyWith(
text: widget.message.text!.length > 200
? '${widget.message.text!.substring(0, 200)}...'
: widget.message.text,
),
showReactions: false,
showUsername: false,
showTimestamp: false,
translateUserAvatar: false,
showSendingIndicator: false,
padding: const EdgeInsets.all(0),
showReactionPickerIndicator: widget.showReactions &&
(widget.message.status == MessageSendingStatus.sent),
showPinHighlight: false,
showUserAvatar:
widget.message.user!.id == channel.client.state.user!.id
? DisplayWidget.gone
: DisplayWidget.show,
),
onCopyTap: (message) => onCopyTap: (message) =>
Clipboard.setData(ClipboardData(text: message.text)), Clipboard.setData(ClipboardData(text: message.text)),
attachmentBorderRadiusGeometry:
widget.attachmentBorderRadiusGeometry as BorderRadius?,
showUserAvatar:
widget.message.user!.id == channel.client.state.user!.id
? DisplayWidget.gone
: DisplayWidget.show,
messageTheme: widget.messageTheme, messageTheme: widget.messageTheme,
messageShape: widget.shape ?? _getDefaultShape(context),
attachmentShape: widget.attachmentShape ??
_getDefaultAttachmentShape(context),
reverse: widget.reverse, reverse: widget.reverse,
showDeleteMessage: widget.showDeleteMessage || isDeleteFailed, showDeleteMessage: widget.showDeleteMessage || isDeleteFailed,
message: widget.message, message: widget.message,
@@ -964,18 +1095,29 @@ class _MessageWidgetState extends State<MessageWidget>
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageReactionsModal( child: MessageReactionsModal(
textBuilder: widget.textBuilder, messageWidget: widget.copyWith(
attachmentBorderRadiusGeometry: key: const Key('MessageWidget'),
widget.attachmentBorderRadiusGeometry as BorderRadius?, message: widget.message.copyWith(
showUserAvatar: text: widget.message.text!.length > 200
widget.message.user!.id == channel.client.state.user!.id ? '${widget.message.text!.substring(0, 200)}...'
? DisplayWidget.gone : widget.message.text,
: DisplayWidget.show, ),
showReactions: false,
showUsername: false,
showTimestamp: false,
translateUserAvatar: false,
showSendingIndicator: false,
padding: const EdgeInsets.all(0),
showReactionPickerIndicator: widget.showReactions &&
(widget.message.status == MessageSendingStatus.sent),
showPinHighlight: false,
showUserAvatar:
widget.message.user!.id == channel.client.state.user!.id
? DisplayWidget.gone
: DisplayWidget.show,
),
onUserAvatarTap: widget.onUserAvatarTap, onUserAvatarTap: widget.onUserAvatarTap,
messageTheme: widget.messageTheme, messageTheme: widget.messageTheme,
messageShape: widget.shape ?? _getDefaultShape(context),
attachmentShape:
widget.attachmentShape ?? _getDefaultAttachmentShape(context),
reverse: widget.reverse, reverse: widget.reverse,
message: widget.message, message: widget.message,
showReactions: widget.showReactions, showReactions: widget.showReactions,
@@ -984,28 +1126,6 @@ class _MessageWidgetState extends State<MessageWidget>
); );
} }
ShapeBorder _getDefaultAttachmentShape(BuildContext context) {
final hasFiles =
widget.message.attachments.any((it) => it.type == 'file') == true;
return RoundedRectangleBorder(
side: hasFiles
? widget.attachmentBorderSide ??
BorderSide(
color: _streamChatTheme.colorTheme.greyWhisper,
)
: BorderSide.none,
borderRadius: widget.attachmentBorderRadiusGeometry ?? BorderRadius.zero,
);
}
ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder(
side: widget.borderSide ??
BorderSide(
color: _streamChatTheme.colorTheme.greyWhisper,
),
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
);
Widget _parseAttachments() { Widget _parseAttachments() {
final attachmentGroups = <String, List<Attachment>>{}; final attachmentGroups = <String, List<Attachment>>{};
@@ -38,6 +38,10 @@ void main() {
id: 'user-id', id: 'user-id',
), ),
), ),
messageWidget: const Text(
'test',
key: Key('MessageWidget'),
),
messageTheme: streamTheme.ownMessageTheme, messageTheme: streamTheme.ownMessageTheme,
), ),
), ),
@@ -86,6 +90,10 @@ void main() {
), ),
), ),
messageTheme: streamTheme.ownMessageTheme, messageTheme: streamTheme.ownMessageTheme,
messageWidget: const Text(
'test',
key: Key('MessageWidget'),
),
), ),
), ),
), ),
@@ -123,6 +131,7 @@ void main() {
client: client, client: client,
child: SizedBox( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: const Text('test'),
message: Message( message: Message(
text: 'test', text: 'test',
user: User( user: User(
@@ -178,6 +187,7 @@ void main() {
client: client, client: client,
child: SizedBox( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: const Text('test'),
onReplyTap: (m) { onReplyTap: (m) {
tapped = true; tapped = true;
}, },
@@ -223,6 +233,7 @@ void main() {
client: client, client: client,
child: SizedBox( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: const Text('test'),
onThreadReplyTap: (m) { onThreadReplyTap: (m) {
tapped = true; tapped = true;
}, },
@@ -272,6 +283,7 @@ void main() {
channel: channel, channel: channel,
child: SizedBox( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: const Text('test'),
message: Message( message: Message(
text: 'test', text: 'test',
user: User( user: User(
@@ -320,6 +332,7 @@ void main() {
channel: channel, channel: channel,
child: SizedBox( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: const Text('test'),
editMessageInputBuilder: (context, m) => const Text('test'), editMessageInputBuilder: (context, m) => const Text('test'),
message: Message( message: Message(
text: 'test', text: 'test',
@@ -371,6 +384,7 @@ void main() {
channel: channel, channel: channel,
child: SizedBox( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: const Text('test'),
onCopyTap: (m) => tapped = true, onCopyTap: (m) => tapped = true,
message: Message( message: Message(
text: 'test', text: 'test',
@@ -420,6 +434,7 @@ void main() {
channel: channel, channel: channel,
child: SizedBox( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: const Text('test'),
message: Message( message: Message(
status: MessageSendingStatus.failed, status: MessageSendingStatus.failed,
text: 'test', text: 'test',
@@ -469,6 +484,7 @@ void main() {
channel: channel, channel: channel,
child: SizedBox( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: const Text('test'),
message: Message( message: Message(
status: MessageSendingStatus.failed_update, status: MessageSendingStatus.failed_update,
text: 'test', text: 'test',
@@ -516,6 +532,7 @@ void main() {
channel: channel, channel: channel,
child: SizedBox( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: const Text('test'),
message: Message( message: Message(
id: 'testid', id: 'testid',
text: 'test', text: 'test',
@@ -571,6 +588,7 @@ void main() {
channel: channel, channel: channel,
child: SizedBox( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: const Text('test'),
message: Message( message: Message(
id: 'testid', id: 'testid',
text: 'test', text: 'test',
@@ -626,6 +644,7 @@ void main() {
channel: channel, channel: channel,
child: SizedBox( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: const Text('test'),
message: Message( message: Message(
id: 'testid', id: 'testid',
text: 'test', text: 'test',
@@ -679,6 +698,7 @@ void main() {
channel: channel, channel: channel,
child: SizedBox( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: const Text('test'),
message: Message( message: Message(
id: 'testid', id: 'testid',
text: 'test', text: 'test',
@@ -734,6 +754,7 @@ void main() {
channel: channel, channel: channel,
child: SizedBox( child: SizedBox(
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: const Text('test'),
message: Message( message: Message(
id: 'testid', id: 'testid',
text: 'test', text: 'test',
@@ -34,6 +34,10 @@ void main() {
client: client, client: client,
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
child: MessageReactionsModal( child: MessageReactionsModal(
messageWidget: const Text(
'test',
key: Key('MessageWidget'),
),
message: message, message: message,
messageTheme: streamTheme.ownMessageTheme, messageTheme: streamTheme.ownMessageTheme,
), ),
@@ -43,28 +47,9 @@ void main() {
await tester.pump(const Duration(milliseconds: 1000)); await tester.pump(const Duration(milliseconds: 1000));
expect(find.byType(MessageWidget), findsOneWidget);
final messageWidget =
tester.widget<MessageWidget>(find.byType(MessageWidget));
expect(messageWidget.message, message);
expect(messageWidget.messageTheme, streamTheme.ownMessageTheme);
expect(messageWidget.attachmentBorderRadiusGeometry, null);
expect(messageWidget.showUserAvatar, DisplayWidget.show);
expect(messageWidget.reverse, false);
expect(messageWidget.attachmentShape, null);
expect(messageWidget.shape, null);
expect(messageWidget.onUserAvatarTap, null);
expect(messageWidget.showReactions, false);
expect(messageWidget.showUsername, false);
expect(messageWidget.showThreadReplyIndicator, false);
expect(messageWidget.showTimestamp, false);
expect(messageWidget.translateUserAvatar, false);
expect(messageWidget.showSendingIndicator, false);
expect(find.byType(ReactionBubble), findsNothing); expect(find.byType(ReactionBubble), findsNothing);
//only one avatar (the message one) expect(find.byType(UserAvatar), findsNothing);
expect(find.byType(UserAvatar), findsOneWidget);
}, },
); );
@@ -105,14 +90,14 @@ void main() {
client: client, client: client,
streamChatThemeData: streamTheme, streamChatThemeData: streamTheme,
child: MessageReactionsModal( child: MessageReactionsModal(
messageWidget: const Text(
'test',
key: Key('MessageWidget'),
),
message: message, message: message,
messageTheme: streamTheme.ownMessageTheme, messageTheme: streamTheme.ownMessageTheme,
showUserAvatar: DisplayWidget.gone,
reverse: true, reverse: true,
attachmentBorderRadiusGeometry: BorderRadius.circular(1),
attachmentShape: const RoundedRectangleBorder(),
showReactions: false, showReactions: false,
messageShape: const RoundedRectangleBorder(),
onUserAvatarTap: onUserAvatarTap, onUserAvatarTap: onUserAvatarTap,
), ),
), ),
@@ -121,27 +106,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 1000)); await tester.pump(const Duration(milliseconds: 1000));
expect(find.byType(MessageWidget), findsOneWidget); expect(find.byKey(const Key('MessageWidget')), findsOneWidget);
final messageWidget =
tester.widget<MessageWidget>(find.byType(MessageWidget));
expect(messageWidget.message, message);
expect(messageWidget.messageTheme, streamTheme.ownMessageTheme);
expect(messageWidget.attachmentBorderRadiusGeometry,
BorderRadius.circular(1));
expect(messageWidget.showUserAvatar, DisplayWidget.gone);
expect(messageWidget.reverse, true);
expect(messageWidget.showReactions, false);
expect(messageWidget.attachmentShape, const RoundedRectangleBorder());
expect(messageWidget.shape, const RoundedRectangleBorder());
expect(messageWidget.showReactions, false);
expect(messageWidget.showUsername, false);
expect(messageWidget.showThreadReplyIndicator, false);
expect(messageWidget.showTimestamp, false);
expect(messageWidget.translateUserAvatar, false);
expect(messageWidget.showSendingIndicator, false);
final userAvatar = tester.widget<UserAvatar>(find.byType(UserAvatar));
expect(userAvatar.onTap, onUserAvatarTap);
expect(find.byType(ReactionBubble), findsOneWidget); expect(find.byType(ReactionBubble), findsOneWidget);
expect(find.byType(UserAvatar), findsOneWidget); expect(find.byType(UserAvatar), findsOneWidget);