Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* **Feat**: [454](https://github.com/SimformSolutionsPvtLtd/chatview/pull/454) :sparkles: Add configurable image fit option for AdaptiveImage widget
* **Feat**: [457](https://github.com/SimformSolutionsPvtLtd/chatview/pull/457) :sparkles: Add errorWidget support to ImageMessageConfiguration
* **Fix**: [458](https://github.com/SimformSolutionsPvtLtd/chatview/pull/458) Enhance text field action animations for improved UX
* **Refactor**: [455](https://github.com/SimformSolutionsPvtLtd/chatview/pull/455) Rework sending receipt animation with different transition

## 3.1.0

Expand Down
8 changes: 8 additions & 0 deletions doc/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,14 @@ ChatView(
fontWeight: FontWeight.w500,
),
),
receiptsWidgetConfig: ReceiptsWidgetConfig(
// Animation style for the pending → sent transition.
// SendingMessageAnimationType.slideOut (default), .textLabel, or .clockToTick.
sendingAnimationType: SendingMessageAnimationType.clockToTick,
// Color of the sending indicator (currently honored by clockToTick).
sendingIndicatorColor: Colors.white70,
),
),
inComingChatBubbleConfig: // Similar to outgoingChatBubbleConfig
),
// ...
Expand Down
16 changes: 16 additions & 0 deletions lib/src/models/config_models/receipts_widget_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
* SOFTWARE.
*/

import 'package:flutter/material.dart';

import '../../utils/constants/constants.dart';
import '../../values/enumeration.dart';
import '../../values/typedefs.dart';
Expand All @@ -29,6 +31,8 @@ class ReceiptsWidgetConfig {
this.receiptsBuilder,
this.lastSeenAgoBuilder,
this.showReceiptsIn = ShowReceiptsIn.lastMessage,
this.sendingAnimationType = SendingMessageAnimationType.slideOut,
this.sendingIndicatorColor,
});

/// The builder that builds widget that right next to the senders message bubble.
Expand All @@ -44,4 +48,16 @@ class ReceiptsWidgetConfig {

/// Whether to show receipts in all messages or not defaults to [ShowReceiptsIn.lastMessage]
final ShowReceiptsIn showReceiptsIn;

/// The animation style used by the default [sendMessageAnimationBuilder] for
/// the pending → sent transition next to an outgoing bubble.
///
/// Ignored when a custom [receiptsBuilder] is provided.
/// Defaults to [SendingMessageAnimationType.slideOut].
final SendingMessageAnimationType sendingAnimationType;

/// Color of the sending indicator. Currently honored only by
/// [SendingMessageAnimationType.clockToTick] (clock / single tick / double
/// tick). When null a default grey is used.
final Color? sendingIndicatorColor;
}
33 changes: 31 additions & 2 deletions lib/src/utils/constants/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import 'package:chatview_utils/chatview_utils.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

import '../../values/enumeration.dart';
import '../../widgets/chat_message_sending_to_sent_animation.dart';
import '../timeago/timeago.dart' as timeago;

Expand Down Expand Up @@ -66,6 +67,30 @@ const double loadMoreCircularProgressIndicatorSize = 36;
const String httpScheme = 'http';
const String httpsScheme = 'https';

/// Diameter of the sending-status indicator circle.
const double sendingIndicatorSize = 6;

/// Default color of the sending-status indicator (dot / text / clock-tick)
/// when no explicit color is provided.
const Color sendingIndicatorDefaultColor = Color(0xFF666f79);

/// Duration of the sending-to-sent indicator slide animation.
const Duration sendingAnimationDuration = Duration(milliseconds: 400);

/// Curve of the sending-to-sent indicator slide animation.
const Curve sendingAnimationCurve = Curves.easeOut;

/// Size of the clock/tick icon for the WhatsApp-style sending indicator.
const double sendingTickIconSize = 14;

/// Vertical space reserved at the bottom of a text bubble for the inline
/// WhatsApp-style clock/tick (and timestamp) so it never overlaps the message.
const double sendingReceiptInBubbleReservedHeight = 18;

/// Text shown by the [SendingMessageAnimationType.textLabel] indicator while a
/// message is pending.
const String sendingLabelText = 'Sending...';

String applicationDateFormatter(DateTime inputTime) {
if (DateTime.now().difference(inputTime).inDays <= 3) {
return timeago.format(inputTime);
Expand All @@ -83,8 +108,12 @@ const String urlRegex =
/// is not sent or at the pending state. A custom implementation can have different
/// widgets for different states.
/// Right now it is implemented to appear right next to the outgoing bubble.
Widget sendMessageAnimationBuilder(MessageStatus status) {
return SendingMessageAnimatingWidget(status);
Widget sendMessageAnimationBuilder(
MessageStatus status, {
SendingMessageAnimationType type = SendingMessageAnimationType.slideOut,
Color? color,
}) {
return SendingMessageAnimatingWidget(status, type: type, color: color);
}

/// Default builder when the message has got seen as of now
Expand Down
39 changes: 39 additions & 0 deletions lib/src/values/enumeration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,45 @@ extension ChatViewStateExtension on ChatViewState {
bool get noMessages => this == ChatViewState.noData;
}

/// Defines how the sending → sent receipt transition is animated next to an
/// outgoing chat bubble.
enum SendingMessageAnimationType {
/// Default. A small dot/circle indicator that slides out to the right once
/// the message transitions from pending to sent.
slideOut,

/// A minimal "Sending..." text label (no surrounding padding) that fades and
/// collapses away once the message is sent.
textLabel,

/// WhatsApp-style. A clock icon shown while the message is pending that
/// cross-fades into a tick (check) once the message is sent.
///
/// For text messages it renders inline at the bubble's bottom-right (next to
/// the timestamp), per-message, so [ShowReceiptsIn.lastMessage] does not
/// limit it — every outgoing bubble shows its own tick, matching the
/// WhatsApp pattern. For non-text messages it renders beside the bubble via
/// the standard receipt slot. The [ReceiptsWidgetConfig.receiptsBuilder]
/// override applies only to that beside-bubble slot, not the inline tick.
clockToTick;

/// Returns true if the animation type is [slideOut].
bool get isSlideOut => this == slideOut;

/// Returns true if the animation type is [textLabel].
bool get isTextLabel => this == textLabel;

/// Returns true if the animation type is [clockToTick].
bool get isClockToTick => this == clockToTick;

/// Human-readable label, handy for settings/menus.
String get label => switch (this) {
slideOut => 'Slide-out dot',
textLabel => 'Sending text',
clockToTick => 'Clock to tick',
};
}

enum GroupedListOrder { asc, desc }

extension GroupedListOrderExtension on GroupedListOrder {
Expand Down
124 changes: 80 additions & 44 deletions lib/src/widgets/chat_bubble_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ class _ChatBubbleWidgetState extends State<ChatBubbleWidget> {
Expanded(
child: _messagesWidgetColumn(messagedUser),
),
if (isMessageBySender) ...[getReceipt()],
if (isMessageBySender &&
(featureActiveConfig?.enableCurrentUserProfileAvatar ?? true))
widget.isLastInGroup
Expand Down Expand Up @@ -246,6 +245,14 @@ class _ChatBubbleWidgetState extends State<ChatBubbleWidget> {
}
}

SendingMessageAnimationType get sendingAnimationType =>
chatListConfig.chatBubbleConfig?.outgoingChatBubbleConfig
?.receiptsWidgetConfig?.sendingAnimationType ??
SendingMessageAnimationType.slideOut;

Color? get sendingIndicatorColor => chatListConfig.chatBubbleConfig
?.outgoingChatBubbleConfig?.receiptsWidgetConfig?.sendingIndicatorColor;

Widget getReceipt() {
final showReceipts = chatListConfig.chatBubbleConfig
?.outgoingChatBubbleConfig?.receiptsWidgetConfig?.showReceiptsIn ??
Expand All @@ -261,7 +268,8 @@ class _ChatBubbleWidgetState extends State<ChatBubbleWidget> {
return chatListConfig.chatBubbleConfig?.outgoingChatBubbleConfig
?.receiptsWidgetConfig?.receiptsBuilder
?.call(value) ??
sendMessageAnimationBuilder(value);
sendMessageAnimationBuilder(value,
type: sendingAnimationType, color: sendingIndicatorColor);
}
return const SizedBox();
},
Expand All @@ -278,9 +286,12 @@ class _ChatBubbleWidgetState extends State<ChatBubbleWidget> {
return chatListConfig.chatBubbleConfig?.outgoingChatBubbleConfig
?.receiptsWidgetConfig?.receiptsBuilder
?.call(value) ??
sendMessageAnimationBuilder(value);
sendMessageAnimationBuilder(value,
type: sendingAnimationType,
color: sendingIndicatorColor);
}
return sendMessageAnimationBuilder(value);
return sendMessageAnimationBuilder(value,
type: sendingAnimationType, color: sendingIndicatorColor);
});
}
return const SizedBox();
Expand Down Expand Up @@ -347,50 +358,75 @@ class _ChatBubbleWidgetState extends State<ChatBubbleWidget> {
),
),
),
SwipeToReply(
isMessageByCurrentUser: isMessageBySender,
onSwipe: isMessageBySender ? onLeftSwipe : onRightSwipe,
child: MessageView(
outgoingChatBubbleConfig:
chatListConfig.chatBubbleConfig?.outgoingChatBubbleConfig,
isLongPressEnable:
(featureActiveConfig?.enableReactionPopup ?? true) ||
(featureActiveConfig?.enableReplySnackBar ?? true),
inComingChatBubbleConfig:
chatListConfig.chatBubbleConfig?.inComingChatBubbleConfig,
message: widget.message,
isMessageBySender: isMessageBySender,
messageConfig: chatListConfig.messageConfig,
onLongPress: widget.onLongPress,
chatBubbleMaxWidth: chatListConfig.chatBubbleConfig?.maxWidth,
longPressAnimationDuration:
chatListConfig.chatBubbleConfig?.longPressAnimationDuration,
onDoubleTap: featureActiveConfig?.enableDoubleTapToLike ?? false
? chatListConfig.chatBubbleConfig?.onDoubleTap ??
(message) => currentUser != null
? chatController?.setReaction(
emoji: heart,
messageId: message.id,
userId: currentUser!.id,
)
: null
: null,
shouldHighlight: widget.shouldHighlight,
controller: chatController,
highlightColor: chatListConfig.repliedMessageConfig
?.repliedMsgAutoScrollConfig.highlightColor ??
Colors.grey,
highlightScale: chatListConfig.repliedMessageConfig
?.repliedMsgAutoScrollConfig.highlightScale ??
1.1,
onMaxDuration: _onMaxDuration,
isFirstInGroup: widget.isFirstInGroup,
isLastInGroup: widget.isLastInGroup,
),
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SwipeToReply(
isMessageByCurrentUser: isMessageBySender,
onSwipe: isMessageBySender ? onLeftSwipe : onRightSwipe,
child: _messageView(),
),
// slideOut indicator sits to the right of the bubble.
if (isMessageBySender && sendingAnimationType.isSlideOut)
getReceipt(),
// clockToTick renders inline inside text bubbles (see
// TextMessageView); for non-text messages there is no inline slot,
// so fall back to showing it beside the bubble.
if (isMessageBySender &&
sendingAnimationType.isClockToTick &&
!widget.message.messageType.isText)
getReceipt(),
],
),
// textLabel indicator sits just below the bubble, aligned to its side.
if (isMessageBySender && sendingAnimationType.isTextLabel)
Padding(
padding: const EdgeInsets.only(right: 6),
child: getReceipt(),
),
],
);
}

Widget _messageView() {
return MessageView(
outgoingChatBubbleConfig:
chatListConfig.chatBubbleConfig?.outgoingChatBubbleConfig,
isLongPressEnable: (featureActiveConfig?.enableReactionPopup ?? true) ||
(featureActiveConfig?.enableReplySnackBar ?? true),
inComingChatBubbleConfig:
chatListConfig.chatBubbleConfig?.inComingChatBubbleConfig,
message: widget.message,
isMessageBySender: isMessageBySender,
messageConfig: chatListConfig.messageConfig,
onLongPress: widget.onLongPress,
chatBubbleMaxWidth: chatListConfig.chatBubbleConfig?.maxWidth,
longPressAnimationDuration:
chatListConfig.chatBubbleConfig?.longPressAnimationDuration,
onDoubleTap: featureActiveConfig?.enableDoubleTapToLike ?? false
? chatListConfig.chatBubbleConfig?.onDoubleTap ??
(message) => currentUser != null
? chatController?.setReaction(
emoji: heart,
messageId: message.id,
userId: currentUser!.id,
)
: null
: null,
shouldHighlight: widget.shouldHighlight,
controller: chatController,
highlightColor: chatListConfig
.repliedMessageConfig?.repliedMsgAutoScrollConfig.highlightColor ??
Colors.grey,
highlightScale: chatListConfig
.repliedMessageConfig?.repliedMsgAutoScrollConfig.highlightScale ??
1.1,
onMaxDuration: _onMaxDuration,
isFirstInGroup: widget.isFirstInGroup,
isLastInGroup: widget.isLastInGroup,
);
}

void _onMaxDuration(int duration) => maxDuration = duration;
}
Loading
Loading