diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 9940ca28..b957f734 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -876,6 +876,23 @@ class Channel { } } + /// Retry the operation on the message based on the failed state. + /// + /// For example, if the message failed to send, it will retry sending the + /// message and vice-versa. + Future retryMessage(Message message) async { + assert(message.state.isFailed, 'Message state is not failed'); + + return message.state.maybeWhen( + failed: (state, _) => state.when( + sendingFailed: () => sendMessage(message), + updatingFailed: () => updateMessage(message), + deletingFailed: (hard) => deleteMessage(message, hard: hard), + ), + orElse: () => throw StateError('Message state is not failed'), + ); + } + /// Pins provided message Future pinMessage( Message message, { diff --git a/packages/stream_chat/lib/src/client/retry_queue.dart b/packages/stream_chat/lib/src/client/retry_queue.dart index 786c4c8f..43ad314d 100644 --- a/packages/stream_chat/lib/src/client/retry_queue.dart +++ b/packages/stream_chat/lib/src/client/retry_queue.dart @@ -89,7 +89,7 @@ class RetryQueue { final retryPolicy = _retryPolicy; try { await backOff( - () => _retryMessage(message), + () => channel.retryMessage(message), delayFactor: retryPolicy.delayFactor, randomizationFactor: retryPolicy.randomizationFactor, maxDelay: retryPolicy.maxDelay, @@ -113,17 +113,6 @@ class RetryQueue { _isProcessing = false; } - Future _retryMessage(Message message) async { - return message.state.maybeWhen( - failed: (state, _) => state.when( - sendingFailed: () => channel.sendMessage(message), - updatingFailed: () => channel.updateMessage(message), - deletingFailed: (hard) => channel.deleteMessage(message, hard: hard), - ), - orElse: () => throw StateError('Message state is not failed'), - ); - } - /// Whether our [_messageQueue] has messages or not. bool get hasMessages => _messageQueue.isNotEmpty; diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart index 9c28b665..c70ed6c1 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart @@ -19,6 +19,7 @@ class MessageActionsModal extends StatefulWidget { this.showDeleteMessage = true, this.showEditMessage = true, this.onReplyTap, + this.onConfirmDeleteTap, this.onThreadReplyTap, this.showCopyMessage = true, this.showReplyMessage = true, @@ -44,6 +45,9 @@ class MessageActionsModal extends StatefulWidget { /// The action to perform when "reply" is tapped final OnMessageTap? onReplyTap; + /// The action to perform when delete confirmation button is tapped. + final Future Function(Message)? onConfirmDeleteTap; + /// Message in focus for actions final Message message; @@ -359,7 +363,12 @@ class _MessageActionsModalState extends State { if (answer == true) { try { Navigator.of(context).pop(); - await StreamChannel.of(context).channel.deleteMessage(widget.message); + final onConfirmDeleteTap = widget.onConfirmDeleteTap; + if (onConfirmDeleteTap != null) { + await onConfirmDeleteTap(widget.message); + } else { + await StreamChannel.of(context).channel.deleteMessage(widget.message); + } } catch (err) { _showErrorAlertBottomSheet(); } diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/resend_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/resend_message_button.dart index 06ed5945..612fe211 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/resend_message_button.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/resend_message_button.dart @@ -27,12 +27,7 @@ class ResendMessageButton extends StatelessWidget { return InkWell( onTap: () { Navigator.of(context).pop(); - message.state.whenOrNull(failed: (state, error) { - state.whenOrNull( - sendingFailed: () => channel.sendMessage(message), - updatingFailed: () => channel.updateMessage(message), - ); - }); + channel.retryMessage(message); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart index a7a02a60..d9f13c6e 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart @@ -129,6 +129,7 @@ class StreamMessageInput extends StatefulWidget { this.customAutocompleteTriggers = const [], this.mentionAllAppUsers = false, this.sendButtonBuilder, + this.quotedMessageBuilder, this.shouldKeepFocusAfterMessage, this.validator = _defaultValidator, this.restorationId, @@ -266,6 +267,9 @@ class StreamMessageInput extends StatefulWidget { /// Builder for creating send button final MessageRelatedBuilder? sendButtonBuilder; + /// Builder for building quoted message + final Widget Function(BuildContext, Message)? quotedMessageBuilder; + /// Defines if the [StreamMessageInput] loses focuses after a message is sent. /// The default behaviour keeps focus until a command is enabled. final bool? shouldKeepFocusAfterMessage; @@ -781,7 +785,6 @@ class StreamMessageInputState extends State Future _onAttachmentButtonPressed() async { final attachments = await showStreamAttachmentPickerModalBottomSheet( context: context, - useRootNavigator: true, allowedTypes: widget.allowedAttachmentPickerTypes, initialAttachments: _effectiveController.attachments, ); @@ -1131,14 +1134,20 @@ class StreamMessageInputState extends State if (!_hasQuotedMessage) return const Offstage(); final containsUrl = _effectiveController.message.quotedMessage!.attachments .any((element) => element.titleLink != null); - return StreamQuotedMessageWidget( - reverse: true, - showBorder: !containsUrl, - message: _effectiveController.message.quotedMessage!, - messageTheme: _streamChatTheme.otherMessageTheme, - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - onQuotedMessageClear: widget.onQuotedMessageCleared, - ); + + return widget.quotedMessageBuilder?.call( + context, + _effectiveController.message.quotedMessage!, + ) ?? + StreamQuotedMessageWidget( + reverse: true, + showBorder: !containsUrl, + message: _effectiveController.message.quotedMessage!, + messageTheme: _streamChatTheme.otherMessageTheme, + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + onQuotedMessageClear: widget.onQuotedMessageCleared, + attachmentThumbnailBuilders: widget.attachmentThumbnailBuilders, + ); } Widget _buildAttachments() { diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart index c823439f..2514dbff 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -61,6 +61,7 @@ class StreamMessageWidget extends StatefulWidget { this.showInChannelIndicator = false, this.onReplyTap, this.onThreadTap, + this.onConfirmDeleteTap, this.showUsername = true, this.showTimestamp = true, this.showReactions = true, @@ -78,6 +79,7 @@ class StreamMessageWidget extends StatefulWidget { this.onMessageActions, this.onShowMessage, this.userAvatarBuilder, + this.quotedMessageBuilder, this.editMessageInputBuilder, this.textBuilder, @Deprecated(''' @@ -307,6 +309,11 @@ class StreamMessageWidget extends StatefulWidget { /// {@endtemplate} final void Function(Message)? onReplyTap; + /// {@template onDeleteTap} + /// The function called when delete confirmation button is tapped. + /// {@endtemplate} + final Future Function(Message)? onConfirmDeleteTap; + /// {@template editMessageInputBuilder} /// Widget builder for edit message layout /// {@endtemplate} @@ -348,6 +355,11 @@ class StreamMessageWidget extends StatefulWidget { /// {@endtemplate} final Widget Function(BuildContext, User)? userAvatarBuilder; + /// {@template quotedMessageBuilder} + /// Widget builder for building quoted message + /// {@endtemplate} + final Widget Function(BuildContext, Message)? quotedMessageBuilder; + /// {@template message} /// The message to display. /// {@endtemplate} @@ -568,8 +580,10 @@ class StreamMessageWidget extends StatefulWidget { void Function(User)? onMentionTap, void Function(Message)? onThreadTap, void Function(Message)? onReplyTap, + Future Function(Message)? onConfirmDeleteTap, Widget Function(BuildContext, Message)? editMessageInputBuilder, Widget Function(BuildContext, Message)? textBuilder, + Widget Function(BuildContext, Message)? quotedMessageBuilder, @Deprecated(''' Use [bottomRowBuilderWithDefaultWidget] instead. Will be removed in the next major version. @@ -659,9 +673,11 @@ class StreamMessageWidget extends StatefulWidget { onMentionTap: onMentionTap ?? this.onMentionTap, onThreadTap: onThreadTap ?? this.onThreadTap, onReplyTap: onReplyTap ?? this.onReplyTap, + onConfirmDeleteTap: onConfirmDeleteTap ?? this.onConfirmDeleteTap, editMessageInputBuilder: editMessageInputBuilder ?? this.editMessageInputBuilder, textBuilder: textBuilder ?? this.textBuilder, + quotedMessageBuilder: quotedMessageBuilder ?? this.quotedMessageBuilder, bottomRowBuilderWithDefaultWidget: _bottomRowBuilderWithDefaultWidget, onMessageActions: onMessageActions ?? this.onMessageActions, message: message ?? this.message, @@ -955,6 +971,7 @@ class _StreamMessageWidgetState extends State borderSide: widget.borderSide, borderRadiusGeometry: widget.borderRadiusGeometry, textBuilder: widget.textBuilder, + quotedMessageBuilder: widget.quotedMessageBuilder, onLinkTap: widget.onLinkTap, onMentionTap: widget.onMentionTap, onQuotedMessageTap: widget.onQuotedMessageTap, @@ -1094,16 +1111,21 @@ class _StreamMessageWidgetState extends State ), onClick: () async { Navigator.of(context, rootNavigator: true).pop(); - final deleted = await showDialog( + final deleted = await showDialog( context: context, barrierDismissible: false, builder: (_) => const DeleteMessageDialog(), ); - if (deleted) { + if (deleted == true) { try { - await StreamChannel.of(context) - .channel - .deleteMessage(widget.message); + final onConfirmDeleteTap = widget.onConfirmDeleteTap; + if (onConfirmDeleteTap != null) { + await onConfirmDeleteTap(widget.message); + } else { + await StreamChannel.of(context) + .channel + .deleteMessage(widget.message); + } } catch (e) { showDialog( context: context, @@ -1175,6 +1197,7 @@ class _StreamMessageWidgetState extends State messageTheme: widget.messageTheme, reverse: widget.reverse, showDeleteMessage: shouldShowDeleteAction, + onConfirmDeleteTap: widget.onConfirmDeleteTap, message: widget.message, editMessageInputBuilder: widget.editMessageInputBuilder, onReplyTap: widget.onReplyTap, diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart index 7cc857d5..92f475c2 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart @@ -63,6 +63,7 @@ class MessageWidgetContent extends StatelessWidget { this.onMentionTap, this.onLinkTap, this.textBuilder, + this.quotedMessageBuilder, @Deprecated(''' Use [bottomRowBuilderWithDefaultWidget] instead. Will be removed in the next major version. @@ -170,6 +171,9 @@ class MessageWidgetContent extends StatelessWidget { /// {@macro textBuilder} final Widget Function(BuildContext, Message)? textBuilder; + /// {@macro quotedMessageBuilder} + final Widget Function(BuildContext, Message)? quotedMessageBuilder; + /// {@macro showReactionPickerIndicator} final bool showReactionPickerIndicator; @@ -351,6 +355,8 @@ class MessageWidgetContent extends StatelessWidget { onMentionTap: onMentionTap, onLinkTap: onLinkTap, textBuilder: textBuilder, + quotedMessageBuilder: + quotedMessageBuilder, borderRadiusGeometry: borderRadiusGeometry, borderSide: borderSide,