diff --git a/example/lib/customize_message_widget.dart b/example/lib/customize_message_widget.dart index a3713b69..84e65eab 100644 --- a/example/lib/customize_message_widget.dart +++ b/example/lib/customize_message_widget.dart @@ -120,7 +120,7 @@ class ChannelPage extends StatelessWidget { top: !details.isLastUser ? Radius.circular(16) : Radius.zero, bottom: !details.isNextUser ? Radius.circular(16) : Radius.zero, ), - showSendingIndicator: DisplayWidget.gone, + showSendingIndicator: false, reverse: false, showUserAvatar: details.isNextUser ? DisplayWidget.hide : DisplayWidget.show, diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index d7497676..9896edc4 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -1,4 +1,3 @@ -import 'dart:math'; import 'dart:ui'; import 'package:flutter/foundation.dart'; @@ -25,8 +24,8 @@ class MessageActionsModal extends StatelessWidget { final bool showCopyMessage; final bool showEditMessage; final bool showResendMessage; - final bool showReply; - final bool showThreadReply; + final bool showReplyMessage; + final bool showThreadReplyMessage; final bool reverse; final ShapeBorder messageShape; final DisplayWidget showUserAvatar; @@ -41,9 +40,9 @@ class MessageActionsModal extends StatelessWidget { this.onReplyTap, this.onThreadReplyTap, this.showCopyMessage = true, - this.showReply = true, + this.showReplyMessage = true, this.showResendMessage = true, - this.showThreadReply = true, + this.showThreadReplyMessage = true, this.showUserAvatar = DisplayWidget.show, this.editMessageInputBuilder, this.messageShape, @@ -130,7 +129,7 @@ class MessageActionsModal extends StatelessWidget { showReactions: false, showUsername: false, showThreadReplyIndicator: false, - showReplyIndicator: false, + showReplyMessage: false, showUserAvatar: showUserAvatar, showTimestamp: false, translateUserAvatar: false, @@ -139,7 +138,7 @@ class MessageActionsModal extends StatelessWidget { MessageSendingStatus.SENT || message.status == null), showInChannelIndicator: false, - showSendingIndicator: DisplayWidget.gone, + showSendingIndicator: false, shape: messageShape, ), ), @@ -181,13 +180,13 @@ class MessageActionsModal extends StatelessWidget { .greyWhisper, context: context, tiles: [ - if (showReply && + if (showReplyMessage && (message.status == MessageSendingStatus.SENT || message.status == null) && message.parentId == null) _buildReplyButton(context), - if (showThreadReply && + if (showThreadReplyMessage && (message.status == MessageSendingStatus.SENT || message.status == null) && diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 415539b8..bea2ad0b 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -220,6 +220,8 @@ class _MessageListViewState extends State { bool get _upToDate => streamChannel.channel.state.isUpToDate; + bool get _isThreadConversation => widget.parentMessage != null; + bool _topPaginationActive = false; bool _bottomPaginationActive = false; @@ -234,7 +236,7 @@ class _MessageListViewState extends State { @override Widget build(BuildContext context) { - final messagesStream = widget.parentMessage != null + final messagesStream = _isThreadConversation ? streamChannel.channel.state.threadsStream .where((threads) => threads.containsKey(widget.parentMessage.id)) .map((threads) => threads[widget.parentMessage.id]) @@ -333,9 +335,8 @@ class _MessageListViewState extends State { physics: widget.scrollPhysics, itemScrollController: _scrollController, reverse: true, - itemCount: messages.length + - 2 + - (widget.parentMessage != null ? 1 : 0), + itemCount: + messages.length + 2 + (_isThreadConversation ? 1 : 0), separatorBuilder: (context, i) { if (i == messages.length) return Offstage(); if (i == messages.length + 2) return Offstage(); @@ -368,7 +369,11 @@ class _MessageListViewState extends State { final isNextUserSame = message.user.id == nextMessage.user?.id; final isThread = message.replyCount > 0; - if (timeDiff >= 1 || !isNextUserSame || isThread) { + final isDeleted = message.isDeleted; + if (timeDiff >= 1 || + !isNextUserSame || + isThread || + isDeleted) { return SizedBox(height: 8); } return SizedBox(height: 2); @@ -497,7 +502,7 @@ class _MessageListViewState extends State { Future _paginateData( StreamChannelState channel, QueryDirection direction) { - if (widget.parentMessage == null) { + if (!_isThreadConversation) { return channel.queryMessages(direction: direction); } else { return channel.getReplies(widget.parentMessage.id); @@ -712,7 +717,12 @@ class _MessageListViewState extends State { return MessageWidget( showThreadReplyIndicator: false, showInChannelIndicator: false, - showReplyIndicator: false, + showReplyMessage: false, + showResendMessage: false, + showThreadReplyMessage: false, + showCopyMessage: false, + showDeleteMessage: false, + showEditMessage: false, message: message, reverse: isMyMessage, showUsername: !isMyMessage, @@ -722,10 +732,8 @@ class _MessageListViewState extends State { right: 8.0, bottom: 16.0, ), - showSendingIndicator: DisplayWidget.hide, + showSendingIndicator: false, onThreadTap: _onThreadTap, - showEditMessage: false, - showDeleteMessage: false, borderRadiusGeometry: BorderRadius.only( topLeft: Radius.circular(16), bottomLeft: Radius.circular(2), @@ -768,32 +776,60 @@ class _MessageListViewState extends State { } final channel = streamChannel.channel; - final readList = channel.state?.read - ?.where((element) => element.user.id != userId) - ?.where((read) => - (read.lastRead.isAfter(message.createdAt) || - read.lastRead.isAtSameMomentAs(message.createdAt)) && - (index == 0 || - read.lastRead.isBefore(messages[index - 1].createdAt))) - ?.toList() ?? + final readList = channel.state?.read?.where((read) { + if (read.user.id == userId) return false; + return (read.lastRead.isAfter(message.createdAt) || + read.lastRead.isAtSameMomentAs(message.createdAt)) && + (index == 0 || + read.lastRead.isBefore(messages[index - 1].createdAt)); + })?.toList() ?? []; final allRead = readList.length >= (channel.memberCount ?? 0) - 1; - - final isThreadMessage = - widget.parentMessage != null || message?.showInChannel == true; - final hasFileAttachment = message.attachments.any((it) => it.type == 'file'); + final isThreadMessage = + message?.parentId != null && message?.showInChannel == true; + + final hasReplies = message.replyCount > 0; + final attachmentBorderRadius = hasFileAttachment ? 12.0 : 14.0; + final showTimeStamp = message.createdAt != null && + (!isThreadMessage || _isThreadConversation) && + !hasReplies && + (timeDiff >= 1 || !isNextUserSame); + + final showUsername = !isMyMessage && + (!isThreadMessage || _isThreadConversation) && + !hasReplies && + (timeDiff >= 1 || !isNextUserSame); + + final showUserAvatar = isMyMessage + ? DisplayWidget.gone + : (timeDiff >= 1 || !isNextUserSame) + ? DisplayWidget.show + : DisplayWidget.hide; + + final showSendingIndicator = + isMyMessage && (index == 0 || timeDiff >= 1 || !isNextUserSame); + + bool showInChannelIndicator = !_isThreadConversation && isThreadMessage; + bool showThreadReplyIndicator = !_isThreadConversation && hasReplies; + Widget child = MessageWidget( key: ValueKey('MESSAGE-${message.id}'), message: message, reverse: isMyMessage, showReactions: !message.isDeleted, padding: const EdgeInsets.symmetric(horizontal: 8.0), + showInChannelIndicator: showInChannelIndicator, + showThreadReplyIndicator: showThreadReplyIndicator, + showUsername: showUsername, + showTimestamp: showTimeStamp, + showSendingIndicator: showSendingIndicator, + showUserAvatar: showUserAvatar, onQuotedMessageTap: (quotedMessageId) async { final scrollToIndex = () { final index = messages.indexWhere((m) => m.id == quotedMessageId); @@ -814,17 +850,9 @@ class _MessageListViewState extends State { }); } }, - showInChannelIndicator: widget.parentMessage == null, - showThreadReplyIndicator: widget.parentMessage == null, - showUsername: !isMyMessage && (timeDiff >= 1 || !isNextUserSame), - showSendingIndicator: - isMyMessage && (index == 0 || timeDiff >= 1 || !isNextUserSame) - ? DisplayWidget.show - : DisplayWidget.hide, - showTimestamp: - !isNextUserSame || readList?.isNotEmpty == true || timeDiff >= 1, showEditMessage: isMyMessage, showDeleteMessage: isMyMessage, + showThreadReplyMessage: !isThreadMessage, borderSide: isMyMessage ? BorderSide.none : null, onThreadTap: _onThreadTap, onReplyTap: widget.onReplyTap, @@ -842,11 +870,6 @@ class _MessageListViewState extends State { topRight: Radius.circular(16), bottomRight: Radius.circular(16), ), - showUserAvatar: isMyMessage - ? DisplayWidget.gone - : (timeDiff >= 1 || !isNextUserSame - ? DisplayWidget.show - : DisplayWidget.hide), messageTheme: isMyMessage ? StreamChatTheme.of(context).ownMessageTheme : StreamChatTheme.of(context).otherMessageTheme, @@ -855,7 +878,7 @@ class _MessageListViewState extends State { onShowMessage: widget.onShowMessage, ); - if (!isThreadMessage) { + if (!message.isDeleted && !message.isSystem && !message.isEphemeral) { child = Swipeable( onSwipeEnd: () => widget.onMessageSwiped(message), backgroundIcon: StreamSvgIcon.reply( @@ -918,7 +941,7 @@ class _MessageListViewState extends State { } }); - if (widget.parentMessage != null) { + if (_isThreadConversation) { streamChannel.getReplies(widget.parentMessage.id); } diff --git a/lib/src/message_reactions_modal.dart b/lib/src/message_reactions_modal.dart index 702bb0ed..5c24e047 100644 --- a/lib/src/message_reactions_modal.dart +++ b/lib/src/message_reactions_modal.dart @@ -116,7 +116,7 @@ class MessageReactionsModal extends StatelessWidget { showThreadReplyIndicator: false, showTimestamp: false, translateUserAvatar: false, - showSendingIndicator: DisplayWidget.gone, + showSendingIndicator: false, shape: messageShape, showInChannelIndicator: false, showReactionPickerIndicator: showReactions && diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index a883201a..e9ea4873 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -95,7 +95,7 @@ class MessageWidget extends StatefulWidget { final DisplayWidget showUserAvatar; /// It controls the display behaviour of the sending indicator - final DisplayWidget showSendingIndicator; + final bool showSendingIndicator; /// If true the widget will show the reactions final bool showReactions; @@ -105,9 +105,6 @@ class MessageWidget extends StatefulWidget { /// If true the widget will show the thread reply indicator final bool showThreadReplyIndicator; - /// If true the widget will show the reply indicator - final bool showReplyIndicator; - /// If true the widget will show the show in channel indicator final bool showInChannelIndicator; @@ -120,9 +117,6 @@ class MessageWidget extends StatefulWidget { /// Used in [MessageReactionsModal] and [MessageActionsModal] final bool showReactionPickerIndicator; - /// If true the widget will show the resendMessage indicator - final bool showResendMessage; - final List readList; final ShowMessageCallback onShowMessage; @@ -130,8 +124,14 @@ class MessageWidget extends StatefulWidget { /// If true show the users username next to the timestamp of the message final bool showUsername; final bool showTimestamp; - final bool showDeleteMessage; + + final bool showReplyMessage; + final bool showThreadReplyMessage; final bool showEditMessage; + final bool showCopyMessage; + final bool showDeleteMessage; + final bool showResendMessage; + final Map attachmentBuilders; /// Center user avatar with bottom of the message @@ -156,10 +156,9 @@ class MessageWidget extends StatefulWidget { this.onMentionTap, this.showReactionPickerIndicator = false, this.showUserAvatar = DisplayWidget.show, - this.showSendingIndicator = DisplayWidget.show, + this.showSendingIndicator = true, this.showThreadReplyIndicator = true, this.showInChannelIndicator = true, - this.showReplyIndicator = true, this.onReplyTap, this.onThreadTap, this.showUsername = true, @@ -167,6 +166,10 @@ class MessageWidget extends StatefulWidget { this.showReactions = true, this.showDeleteMessage = true, this.showEditMessage = true, + this.showReplyMessage = true, + this.showThreadReplyMessage = true, + this.showResendMessage = true, + this.showCopyMessage = true, this.onUserAvatarTap, this.onLinkTap, this.onMessageActions, @@ -174,7 +177,6 @@ class MessageWidget extends StatefulWidget { this.editMessageInputBuilder, this.textBuilder, Map customAttachmentBuilders, - this.showResendMessage = true, this.readList, this.padding, this.textPadding = const EdgeInsets.symmetric( @@ -238,18 +240,19 @@ class MessageWidget extends StatefulWidget { } class _MessageWidgetState extends State { - bool get showThreadReplyIndicator => - widget.showThreadReplyIndicator && widget.message.replyCount > 0; + bool get showThreadReplyIndicator => widget.showThreadReplyIndicator; + + bool get showSendingIndicator => widget.showSendingIndicator; + + bool get isDeleted => widget.message.isDeleted; bool get showUsername => widget.showUsername; - bool get showTimeStamp => - widget.message.createdAt != null && widget.showTimestamp; + bool get showTimeStamp => widget.showTimestamp; bool get isMessageRead => widget.readList?.isNotEmpty == true; - bool get showInChannel => - widget.showInChannelIndicator && widget.message?.showInChannel == true; + bool get showInChannel => widget.showInChannelIndicator; bool get hasQuotedMessage => widget.message?.quotedMessage != null; @@ -271,7 +274,9 @@ class _MessageWidgetState extends State { showThreadReplyIndicator || showUsername || showTimeStamp || - showInChannel; + showInChannel || + showSendingIndicator || + isDeleted; @override Widget build(BuildContext context) { @@ -446,7 +451,11 @@ class _MessageWidgetState extends State { if (showBottomRow) SizedBox(height: 20.0), ], ), - if (showBottomRow) _buildBottomRow(leftPadding), + if (showBottomRow) + Padding( + padding: EdgeInsets.only(left: leftPadding), + child: _bottomRow, + ), if (isFailedState) Positioned( left: widget.reverse ? -3 : null, @@ -475,12 +484,12 @@ class _MessageWidgetState extends State { Widget _buildQuotedMessage() { final isMyMessage = widget.message.user.id == StreamChat.of(context).user.id; + final onTap = widget.message?.quotedMessage?.isDeleted != true && + widget.onQuotedMessageTap != null + ? () => widget.onQuotedMessageTap(widget.message.quotedMessageId) + : null; return QuotedMessageWidget( - onTap: () { - if (widget.onQuotedMessageTap != null) { - widget.onQuotedMessageTap(widget.message.quotedMessageId); - } - }, + onTap: onTap, message: widget.message.quotedMessage, messageTheme: isMyMessage ? StreamChatTheme.of(context).otherMessageTheme @@ -489,12 +498,12 @@ class _MessageWidgetState extends State { ); } - Widget _buildBottomRow(double leftPadding) { - final deleted = widget.message.isDeleted; - var children = []; - if (deleted) { - children.add( - Row( + Widget get _bottomRow { + if (isDeleted) { + return Transform( + transform: Matrix4.rotationY(widget.reverse ? pi : 0), + alignment: Alignment.center, + child: Row( mainAxisSize: MainAxisSize.min, children: [ StreamSvgIcon.eye( @@ -512,102 +521,94 @@ class _MessageWidgetState extends State { ], ), ); - } else if (showInChannel) { - final onThreadTap = () async { - try { + } + + var children = []; + + final threadParticipants = widget.message.threadParticipants; + final showThreadParticipants = threadParticipants?.isNotEmpty == true; + final replyCount = widget.message.replyCount; + + var msg = 'Thread Reply'; + if (showThreadReplyIndicator && replyCount > 1) { + msg = '$replyCount Thread Replies'; + } + + final onThreadTap = () async { + try { + var message = widget.message; + if (showInChannel) { final channel = StreamChannel.of(context); - final message = await channel.getMessage(widget.message.parentId); - return widget.onThreadTap(message); - } catch (e, stk) { - print(e); - print(stk); - return null; + message = await channel.getMessage(widget.message.parentId); } - }; - children.add( + return widget.onThreadTap(message); + } catch (e, stk) { + print(e); + print(stk); + return null; + } + }; + + children.addAll([ + if (showSendingIndicator) _buildSendingIndicator(), + if (showInChannel || showThreadReplyIndicator) ...[ + if (showThreadParticipants) + SizedBox.fromSize( + size: Size((threadParticipants.length * 8.0) + 10, 16), + child: _buildThreadParticipantsIndicator(), + ), InkWell( onTap: widget.onThreadTap != null ? onThreadTap : null, - child: Text('Thread Reply', style: widget.messageTheme?.replies), + child: Text(msg, style: widget.messageTheme?.replies), ), - ); - } else { - final showSendingIndicator = - widget.showSendingIndicator == DisplayWidget.show; - final threadParticipants = widget.message.threadParticipants; - final showThreadParticipants = threadParticipants?.isNotEmpty == true; - final replyCount = widget.message.replyCount; - final msg = replyCount != 0 - ? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}' - : 'Thread Reply'; + ], + if (showUsername) + Text( + widget.message.user.name, + style: widget.messageTheme.replies.copyWith( + color: widget.messageTheme.createdAt.color, + ), + ), + if (showTimeStamp) + Text( + Jiffy(widget.message.createdAt.toLocal()).jm, + style: widget.messageTheme.createdAt, + ), + ]); - final onThreadTap = () async { - var message = widget.message; - return widget.onThreadTap(message); - }; - - children.addAll([ - if (showSendingIndicator) _buildSendingIndicator(), - if (showThreadReplyIndicator) ...[ - if (showThreadParticipants) - SizedBox.fromSize( - size: Size((threadParticipants.length * 8.0) + 10, 16), - child: _buildThreadParticipantsIndicator(), - ), - InkWell( - onTap: widget.onThreadTap != null ? onThreadTap : null, - child: Text(msg, style: widget.messageTheme?.replies), - ), - ], - if (showUsername) - Text( - widget.message.user.name, - style: widget.messageTheme.replies.copyWith( - color: widget.messageTheme.createdAt.color, - ), - ), - if (showTimeStamp) - Text( - Jiffy(widget.message.createdAt.toLocal()).jm, - style: widget.messageTheme.createdAt, - ), - ]); - } if (widget.reverse) children = children.reversed.toList(); - return Padding( - padding: EdgeInsets.only(left: leftPadding), - child: Flex( - direction: Axis.horizontal, - clipBehavior: Clip.none, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (!deleted && (showThreadReplyIndicator || showInChannel)) - Container( - margin: EdgeInsets.only( - bottom: widget.messageTheme.replies.fontSize / 2, - ), - child: CustomPaint( - size: const Size(16, 32), - painter: _ThreadReplyPainter( - context: context, - color: widget.messageTheme.messageBorderColor, - ), - ), + return Flex( + direction: Axis.horizontal, + clipBehavior: Clip.none, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (showThreadReplyIndicator || showInChannel) + Container( + margin: EdgeInsets.only( + bottom: widget.messageTheme.replies.fontSize / 2, ), - ...children.map( - (child) => Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: Container( - height: 16, - child: Center( - child: child, - ), + child: CustomPaint( + size: const Size(16, 32), + painter: _ThreadReplyPainter( + context: context, + color: widget.messageTheme.messageBorderColor, ), ), ), - ].insertBetween(const SizedBox(width: 8.0)), - ), + ...children.map( + (child) => Transform( + transform: Matrix4.rotationY(widget.reverse ? pi : 0), + alignment: Alignment.center, + child: Container( + height: 16, + child: Center( + child: child, + ), + ), + ), + ), + ].insertBetween(const SizedBox(width: 8.0)), ); } @@ -702,14 +703,15 @@ class _MessageWidgetState extends State { messageTheme: widget.messageTheme, messageShape: widget.shape ?? _getDefaultShape(context), reverse: widget.reverse, - showDeleteMessage: widget.showDeleteMessage, + showDeleteMessage: widget.showDeleteMessage || isDeleteFailed, message: widget.message, editMessageInputBuilder: widget.editMessageInputBuilder, onReplyTap: widget.onReplyTap, onThreadReplyTap: widget.onThreadTap, showResendMessage: widget.showResendMessage && (isSendFailed || isUpdateFailed), - showCopyMessage: !isFailedState && + showCopyMessage: widget.showCopyMessage && + !isFailedState && widget.message.text?.trim()?.isNotEmpty == true, showEditMessage: widget.showEditMessage && !isDeleteFailed && @@ -717,11 +719,12 @@ class _MessageWidgetState extends State { ?.any((element) => element.type == 'giphy') != true, showReactions: widget.showReactions, - showReply: widget.showReplyIndicator && + showReplyMessage: widget.showReplyMessage && !isFailedState && widget.onReplyTap != null, - showThreadReply: - widget.showThreadReplyIndicator && widget.onThreadTap != null, + showThreadReplyMessage: widget.showThreadReplyMessage && + !isFailedState && + widget.onThreadTap != null, ), ); }); diff --git a/test/src/message_action_modal_test.dart b/test/src/message_action_modal_test.dart index 7da2095d..3cb916d1 100644 --- a/test/src/message_action_modal_test.dart +++ b/test/src/message_action_modal_test.dart @@ -70,8 +70,8 @@ void main() { showEditMessage: false, showCopyMessage: false, showDeleteMessage: false, - showReply: false, - showThreadReply: false, + showReplyMessage: false, + showThreadReplyMessage: false, message: Message( text: 'test', user: User(