Merge pull request #189 from GetStream/messg-error-state

Message error state
This commit is contained in:
Salvatore Giordano
2020-12-31 11:52:33 +01:00
committed by GitHub
7 changed files with 169 additions and 151 deletions
+5 -1
View File
@@ -122,7 +122,11 @@ class ChannelPreview extends StatelessWidget {
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4.0),
child: SendingIndicator( child: SendingIndicator(
message: channel.state.lastMessage, message: channel.state.lastMessage,
allRead: channel.state.read size: StreamChatTheme.of(context)
.channelPreviewTheme
.lastMessageAt
.fontSize,
isMessageRead: channel.state.read
.where((element) => element.lastRead .where((element) => element.lastRead
.isAfter(channel .isAfter(channel
.state.lastMessage.createdAt)) .state.lastMessage.createdAt))
+36 -6
View File
@@ -22,6 +22,7 @@ class MessageActionsModal extends StatelessWidget {
final bool showDeleteMessage; final bool showDeleteMessage;
final bool showCopyMessage; final bool showCopyMessage;
final bool showEditMessage; final bool showEditMessage;
final bool showResendMessage;
final bool showReply; final bool showReply;
final bool reverse; final bool reverse;
final ShapeBorder messageShape; final ShapeBorder messageShape;
@@ -37,6 +38,7 @@ class MessageActionsModal extends StatelessWidget {
this.onThreadTap, this.onThreadTap,
this.showCopyMessage = true, this.showCopyMessage = true,
this.showReply = true, this.showReply = true,
this.showResendMessage = true,
this.showUserAvatar = DisplayWidget.show, this.showUserAvatar = DisplayWidget.show,
this.editMessageInputBuilder, this.editMessageInputBuilder,
this.messageShape, this.messageShape,
@@ -117,7 +119,10 @@ class MessageActionsModal extends StatelessWidget {
showUserAvatar: showUserAvatar, showUserAvatar: showUserAvatar,
showTimestamp: false, showTimestamp: false,
translateUserAvatar: false, translateUserAvatar: false,
showReactionPickerIndicator: true, showReactionPickerIndicator: showReactions &&
(message.status ==
MessageSendingStatus.SENT ||
message.status == null),
showInChannelIndicator: false, showInChannelIndicator: false,
showSendingIndicator: DisplayWidget.gone, showSendingIndicator: DisplayWidget.gone,
shape: messageShape, shape: messageShape,
@@ -125,9 +130,7 @@ class MessageActionsModal extends StatelessWidget {
), ),
); );
}), }),
SizedBox( SizedBox(height: 8),
height: 8,
),
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0), tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
@@ -159,6 +162,8 @@ class MessageActionsModal extends StatelessWidget {
message.status == null) && message.status == null) &&
message.parentId == null) message.parentId == null)
_buildReplyButton(context), _buildReplyButton(context),
if (showResendMessage)
_buildResendMessage(context),
if (showEditMessage) if (showEditMessage)
_buildEditMessage(context), _buildEditMessage(context),
if (showDeleteMessage) if (showDeleteMessage)
@@ -183,9 +188,10 @@ class MessageActionsModal extends StatelessWidget {
} }
Widget _buildDeleteButton(BuildContext context) { Widget _buildDeleteButton(BuildContext context) {
final isDeleteFailed = message.status == MessageSendingStatus.FAILED_DELETE;
return ListTile( return ListTile(
title: Text( title: Text(
'Delete message', isDeleteFailed ? 'Retry deleting message' : 'Delete message',
style: style:
Theme.of(context).textTheme.headline6.copyWith(color: Colors.red), Theme.of(context).textTheme.headline6.copyWith(color: Colors.red),
), ),
@@ -234,6 +240,29 @@ class MessageActionsModal extends StatelessWidget {
); );
} }
Widget _buildResendMessage(BuildContext context) {
final isUpdateFailed = message.status == MessageSendingStatus.FAILED_UPDATE;
return ListTile(
title: Text(
isUpdateFailed ? 'Resend edited message' : 'Resend',
style: Theme.of(context).textTheme.headline6,
),
leading: StreamSvgIcon.circle_up(
color: StreamChatTheme.of(context).accentColor,
),
onTap: () {
Navigator.pop(context);
final client = StreamChat.of(context).client;
final channel = StreamChannel.of(context).channel;
if (isUpdateFailed) {
client.updateMessage(message, channel.cid);
} else {
channel.sendMessage(message);
}
},
);
}
void _showEditBottomSheet(BuildContext context) { void _showEditBottomSheet(BuildContext context) {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
showModalBottomSheet( showModalBottomSheet(
@@ -301,9 +330,10 @@ class MessageActionsModal extends StatelessWidget {
? editMessageInputBuilder(context, message) ? editMessageInputBuilder(context, message)
: MessageInput( : MessageInput(
editMessage: message, editMessage: message,
onMessageSent: (_) { preMessageSending: (m) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
Navigator.pop(context); Navigator.pop(context);
return m;
}, },
), ),
), ),
-4
View File
@@ -1976,10 +1976,6 @@ class MessageInputState extends State<MessageInput> {
return sendingFuture.then((resp) { return sendingFuture.then((resp) {
if (widget.onMessageSent != null) { if (widget.onMessageSent != null) {
widget.onMessageSent(resp.message); widget.onMessageSent(resp.message);
} else {
if (widget.editMessage != null) {
Navigator.pop(context);
}
} }
}); });
} }
+6 -4
View File
@@ -110,14 +110,16 @@ class MessageReactionsModal extends StatelessWidget {
translateUserAvatar: false, translateUserAvatar: false,
showSendingIndicator: DisplayWidget.gone, showSendingIndicator: DisplayWidget.gone,
shape: messageShape, shape: messageShape,
showReactionPickerIndicator: true, showInChannelIndicator: false,
showReactionPickerIndicator: showReactions &&
(message.status ==
MessageSendingStatus.SENT ||
message.status == null),
), ),
), ),
); );
}), }),
SizedBox( SizedBox(height: 8),
height: 16,
),
if (message.latestReactions?.isNotEmpty == true) if (message.latestReactions?.isNotEmpty == true)
_buildReactionCard(context), _buildReactionCard(context),
], ],
+89 -120
View File
@@ -114,6 +114,9 @@ class MessageWidget extends StatefulWidget {
/// Used in [MessageReactionsModal] and [MessageActionsModal] /// Used in [MessageReactionsModal] and [MessageActionsModal]
final bool showReactionPickerIndicator; final bool showReactionPickerIndicator;
/// If true the widget will show the resendMessage indicator
final bool showResendMessage;
final List<Read> readList; final List<Read> readList;
/// If true show the users username next to the timestamp of the message /// If true show the users username next to the timestamp of the message
@@ -157,6 +160,7 @@ class MessageWidget extends StatefulWidget {
this.editMessageInputBuilder, this.editMessageInputBuilder,
this.textBuilder, this.textBuilder,
Map<String, AttachmentBuilder> customAttachmentBuilders, Map<String, AttachmentBuilder> customAttachmentBuilders,
this.showResendMessage = true,
this.readList, this.readList,
this.padding, this.padding,
this.textPadding = const EdgeInsets.symmetric( this.textPadding = const EdgeInsets.symmetric(
@@ -224,11 +228,27 @@ class _MessageWidgetState extends State<MessageWidget> {
bool get showTimeStamp => bool get showTimeStamp =>
widget.message.createdAt != null && widget.showTimestamp; widget.message.createdAt != null && widget.showTimestamp;
bool get showReadList => widget.readList?.isNotEmpty == true; bool get isMessageRead => widget.readList?.isNotEmpty == true;
bool get showInChannel => bool get showInChannel =>
widget.showInChannelIndicator && widget.message?.showInChannel == true; widget.showInChannelIndicator && widget.message?.showInChannel == true;
bool get isSendFailed => widget.message.status == MessageSendingStatus.FAILED;
bool get isUpdateFailed =>
widget.message.status == MessageSendingStatus.FAILED_UPDATE;
bool get isDeleteFailed =>
widget.message.status == MessageSendingStatus.FAILED_DELETE;
bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed;
bool get showBottomRow =>
showThreadReplyIndicator ||
showUsername ||
showTimeStamp ||
showInChannel;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var leftPadding = widget.showUserAvatar != DisplayWidget.gone var leftPadding = widget.showUserAvatar != DisplayWidget.gone
@@ -253,6 +273,7 @@ class _MessageWidgetState extends State<MessageWidget> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Stack( Stack(
clipBehavior: Clip.none,
alignment: AlignmentDirectional.bottomStart, alignment: AlignmentDirectional.bottomStart,
children: [ children: [
Column( Column(
@@ -298,9 +319,7 @@ class _MessageWidgetState extends State<MessageWidget> {
) )
: EdgeInsets.zero, : EdgeInsets.zero,
child: (widget.message.isDeleted && child: (widget.message.isDeleted &&
widget.message.status != !isFailedState)
MessageSendingStatus
.FAILED_DELETE)
? Transform( ? Transform(
alignment: Alignment.center, alignment: Alignment.center,
transform: Matrix4.rotationY( transform: Matrix4.rotationY(
@@ -383,18 +402,23 @@ class _MessageWidgetState extends State<MessageWidget> {
), ),
], ],
), ),
if (showThreadReplyIndicator || if (showBottomRow) SizedBox(height: 20.0),
showUsername ||
showTimeStamp ||
showInChannel)
SizedBox(height: 20.0),
], ],
), ),
if (showThreadReplyIndicator || if (showBottomRow) _buildBottomRow(leftPadding),
showUsername || if (isFailedState)
showTimeStamp || Positioned(
showInChannel) left: widget.reverse ? -3 : null,
_buildBottomRow(leftPadding) right: widget.reverse ? null : -9,
bottom: showBottomRow ? 20 : 0,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: StreamSvgIcon.error(size: 20),
),
),
], ],
), ),
], ],
@@ -461,14 +485,14 @@ class _MessageWidgetState extends State<MessageWidget> {
children.addAll([ children.addAll([
if (showSendingIndicator) _buildSendingIndicator(), if (showSendingIndicator) _buildSendingIndicator(),
if (showReadList) // if (showReadList)
SizedBox.fromSize( // SizedBox.fromSize(
size: Size((widget.readList.length * 10.0) + 10, 17), // size: Size((widget.readList.length * 10.0) + 10, 17),
child: Padding( // child: Padding(
padding: const EdgeInsets.only(left: 4.0), // padding: const EdgeInsets.only(left: 4.0),
child: _buildReadIndicator(), // child: _buildReadIndicator(),
), // ),
), // ),
if (showThreadReplyIndicator) if (showThreadReplyIndicator)
InkWell( InkWell(
onTap: widget.onThreadTap != null ? onThreadTap : null, onTap: widget.onThreadTap != null ? onThreadTap : null,
@@ -619,14 +643,19 @@ class _MessageWidgetState extends State<MessageWidget> {
message: widget.message, message: widget.message,
editMessageInputBuilder: widget.editMessageInputBuilder, editMessageInputBuilder: widget.editMessageInputBuilder,
onThreadTap: widget.onThreadTap, onThreadTap: widget.onThreadTap,
showCopyMessage: widget.message.text?.trim()?.isNotEmpty == true, showResendMessage:
widget.showResendMessage && (isSendFailed || isUpdateFailed),
showCopyMessage: !isFailedState &&
widget.message.text?.trim()?.isNotEmpty == true,
showEditMessage: widget.showEditMessage && showEditMessage: widget.showEditMessage &&
!isDeleteFailed &&
widget.message.attachments widget.message.attachments
?.any((element) => element.type == 'giphy') != ?.any((element) => element.type == 'giphy') !=
true, true,
showReactions: widget.showReactions, showReactions: widget.showReactions,
showReply: showReply: widget.showThreadReplyIndicator &&
widget.showThreadReplyIndicator && widget.onThreadTap != null, !isFailedState &&
widget.onThreadTap != null,
), ),
); );
}); });
@@ -729,7 +758,6 @@ class _MessageWidgetState extends State<MessageWidget> {
final attachmentShape = final attachmentShape =
widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context); widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context);
return GestureDetector( return GestureDetector(
onTap: () => retryMessage(context),
onLongPress: () => onLongPress(context), onLongPress: () => onLongPress(context),
child: Material( child: Material(
color: _getBackgroundColor(), color: _getBackgroundColor(),
@@ -744,17 +772,7 @@ class _MessageWidgetState extends State<MessageWidget> {
child: Transform( child: Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0), transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center, alignment: Alignment.center,
child: Column( child: attachmentWidget,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
getFailedMessageWidget(
context,
padding: const EdgeInsets.all(8.0),
),
attachmentWidget,
],
),
), ),
), ),
), ),
@@ -777,14 +795,27 @@ class _MessageWidgetState extends State<MessageWidget> {
} }
Widget _buildSendingIndicator() { Widget _buildSendingIndicator() {
return Container( final style = widget.messageTheme.createdAt;
height: widget.messageTheme.createdAt.fontSize + 2, Widget child = SendingIndicator(
width: widget.messageTheme.createdAt.fontSize + 2, message: widget.message,
child: SendingIndicator( isMessageRead: isMessageRead,
message: widget.message, size: style.fontSize,
allRead: widget.allRead,
),
); );
if (isMessageRead) {
child = Row(
children: [
Text(
widget.readList.length.toString(),
style: style.copyWith(
color: StreamChatTheme.of(context).accentColor,
),
),
SizedBox(width: 2),
child,
],
);
}
return child;
} }
Widget _buildUserAvatar() => Transform( Widget _buildUserAvatar() => Transform(
@@ -809,55 +840,6 @@ class _MessageWidgetState extends State<MessageWidget> {
), ),
); );
Widget getFailedMessageWidget(
BuildContext context, {
EdgeInsetsGeometry padding,
}) {
Widget failedWidget;
if (widget.message.status == MessageSendingStatus.FAILED) {
failedWidget = Text(
'MESSAGE FAILED · CLICK TO TRY AGAIN',
style: widget.messageTheme.messageText.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(.5)
: Colors.black.withOpacity(.5),
fontSize: 11,
),
);
}
if (widget.message.status == MessageSendingStatus.FAILED_UPDATE) {
failedWidget = Text(
'MESSAGE UPDATE FAILED · CLICK TO TRY AGAIN',
style: widget.messageTheme.messageText.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(.5)
: Colors.black.withOpacity(.5),
fontSize: 11,
),
);
}
if (widget.message.status == MessageSendingStatus.FAILED_DELETE) {
failedWidget = Text(
'MESSAGE DELETE FAILED · CLICK TO TRY AGAIN',
style: widget.messageTheme.messageText.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(.5)
: Colors.black.withOpacity(.5),
fontSize: 11,
),
);
}
if (failedWidget != null) {
return Padding(
padding: padding ?? EdgeInsets.zero,
child: failedWidget,
);
}
return SizedBox();
}
Widget _buildTextBubble(BuildContext context) { Widget _buildTextBubble(BuildContext context) {
Widget child = Transform( Widget child = Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0), transform: Matrix4.rotationY(widget.reverse ? pi : 0),
@@ -867,26 +849,20 @@ class _MessageWidgetState extends State<MessageWidget> {
children: [ children: [
Padding( Padding(
padding: widget.textPadding, padding: widget.textPadding,
child: Column( child: widget.textBuilder != null
crossAxisAlignment: CrossAxisAlignment.end, ? widget.textBuilder(context, widget.message)
children: [ : MessageText(
getFailedMessageWidget(context), onLinkTap: widget.onLinkTap,
widget.textBuilder != null message: widget.message,
? widget.textBuilder(context, widget.message) onMentionTap: widget.onMentionTap,
: MessageText( messageTheme: isOnlyEmoji
onLinkTap: widget.onLinkTap, ? widget.messageTheme.copyWith(
message: widget.message, messageText:
onMentionTap: widget.onMentionTap, widget.messageTheme.messageText.copyWith(
messageTheme: isOnlyEmoji fontSize: 40,
? widget.messageTheme.copyWith( ))
messageText: : widget.messageTheme,
widget.messageTheme.messageText.copyWith( ),
fontSize: 40,
))
: widget.messageTheme,
),
],
),
), ),
if (widget.message.attachments if (widget.message.attachments
?.any((element) => element.ogScrapeUrl != null) == ?.any((element) => element.ogScrapeUrl != null) ==
@@ -896,7 +872,6 @@ class _MessageWidgetState extends State<MessageWidget> {
), ),
); );
return GestureDetector( return GestureDetector(
onTap: () => retryMessage(context),
onLongPress: () => onLongPress(context), onLongPress: () => onLongPress(context),
child: child, child: child,
); );
@@ -907,12 +882,6 @@ class _MessageWidgetState extends State<MessageWidget> {
widget.message.text.characters.every((c) => Emoji.byChar(c) != null); widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
Color _getBackgroundColor() { Color _getBackgroundColor() {
if ((widget.message.status == MessageSendingStatus.FAILED ||
widget.message.status == MessageSendingStatus.FAILED_UPDATE ||
widget.message.status == MessageSendingStatus.FAILED_DELETE)) {
return Color(0xffd0021B).withOpacity(.1);
}
if (widget.message.attachments if (widget.message.attachments
?.any((element) => element.ogScrapeUrl != null) == ?.any((element) => element.ogScrapeUrl != null) ==
true) { true) {
+9 -16
View File
@@ -4,27 +4,29 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Used to show the sending status of the message /// Used to show the sending status of the message
class SendingIndicator extends StatelessWidget { class SendingIndicator extends StatelessWidget {
final Message message; final Message message;
final bool allRead; final bool isMessageRead;
final double size;
const SendingIndicator({ const SendingIndicator({
Key key, Key key,
this.message, this.message,
this.allRead = false, this.isMessageRead = false,
this.size = 12,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (allRead) { if (isMessageRead) {
return Icon( return Icon(
Icons.done_all, Icons.done_all_rounded,
size: 8, size: size,
color: StreamChatTheme.of(context).accentColor, color: StreamChatTheme.of(context).accentColor,
); );
} }
if (message.status == MessageSendingStatus.SENT || message.status == null) { if (message.status == MessageSendingStatus.SENT || message.status == null) {
return Icon( return Icon(
Icons.done, Icons.done,
size: 8, size: size,
color: IconTheme.of(context).color.withOpacity(0.5), color: IconTheme.of(context).color.withOpacity(0.5),
); );
} }
@@ -32,18 +34,9 @@ class SendingIndicator extends StatelessWidget {
message.status == MessageSendingStatus.UPDATING) { message.status == MessageSendingStatus.UPDATING) {
return Icon( return Icon(
Icons.access_time, Icons.access_time,
size: 8, size: size,
); );
} }
if (message.status == MessageSendingStatus.FAILED ||
message.status == MessageSendingStatus.FAILED_UPDATE ||
message.status == MessageSendingStatus.FAILED_DELETE) {
return Icon(
Icons.error_outline,
size: 8,
);
}
return SizedBox(); return SizedBox();
} }
} }
+24
View File
@@ -757,4 +757,28 @@ class StreamSvgIcon extends StatelessWidget {
height: size, height: size,
); );
} }
factory StreamSvgIcon.error({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_error.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.circle_up({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_circle_up.svg',
color: color,
width: size,
height: size,
);
}
} }