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 @@ -4,6 +4,7 @@
Rendering issue in attached image preview when sending message on web.
* **Feat**: [420](https://github.com/SimformSolutionsPvtLtd/chatview/pull/420) Added support for
`playerMode` in `VoiceMessageConfiguration` with `single` and `multi`.
* **Feat**: [115](https://github.com/SimformSolutionsPvtLtd/chatview/issues/115) Add customizable timestamp styling for chat bubbles and improve timestamp rendering logic.

## [3.0.0]

Expand Down
25 changes: 24 additions & 1 deletion doc/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,14 @@ ChatView(
topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12),
),
// Style for the timestamp text.
// Applies to both in-bubble timestamps (showTimestamp: true)
// and the swipe-to-reveal timestamp (enableSwipeToSeeTime: true).
messageTimeTextStyle: const TextStyle(
color: Colors.white70,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
inComingChatBubbleConfig: ChatBubble(
color: Colors.grey.shade200,
Expand All @@ -736,12 +744,27 @@ ChatView(
topRight: Radius.circular(12),
bottomRight: Radius.circular(12),
),
// Style for the timestamp text.
// Applies to both in-bubble timestamps (showTimestamp: true)
// and the swipe-to-reveal timestamp (enableSwipeToSeeTime: true).
messageTimeTextStyle: const TextStyle(
color: Colors.black54,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
// ...
)
```

Timestamps rendered in bubbles use 12-hour format with AM/PM (for example, `04:32 PM`).
You can control timestamp typography per bubble side using `ChatBubble.messageTimeTextStyle`.

`ChatBubble.messageTimeTextStyle` applies to **both** timestamp display modes:
- **In-bubble** (`FeatureActiveConfig.showTimestamp: true`) — text style inside the bubble.
- **Swipe-to-reveal** (`FeatureActiveConfig.enableSwipeToSeeTime: true`) — per-bubble style for the swipe-out timestamp. When set, this takes priority over the global `MessageListConfiguration.messageTimeTextStyle` fallback.

## Swipe to Reply Configuration

```dart
Expand Down Expand Up @@ -847,7 +870,7 @@ ChatView(
// ...
featureActiveConfig: FeatureActiveConfig(
enableSwipeToReply: true,
enableSwipeToSeeTime: false,
enableSwipeToSeeTime: true,
enablePagination: true,
enableOtherUserName: false,
lastSeenAgoBuilderVisibility: false,
Expand Down
2 changes: 1 addition & 1 deletion lib/src/extensions/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ extension TimeDifference on DateTime {
return formatter.format(this);
}

String get getTimeFromDateTime => DateFormat.Hm().format(this);
String get getTimeFromDateTime => DateFormat('hh:mm a').format(this);

/// Returns `true` if [other] occurs on the same calendar day as
/// this [DateTime].
Expand Down
10 changes: 10 additions & 0 deletions lib/src/models/chat_bubble.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class ChatBubble {
this.color,
this.borderRadius,
this.textStyle,
this.messageTimeTextStyle,
this.padding,
this.margin,
this.linkPreviewConfig,
Expand All @@ -50,6 +51,15 @@ class ChatBubble {
/// Used for giving text style of chat bubble.
final TextStyle? textStyle;

/// Used for giving text style of the message timestamp.
///
/// This applies to:
/// - In-bubble timestamps when `FeatureActiveConfig.showTimestamp` is `true`.
/// - The swipe-to-reveal timestamp when `FeatureActiveConfig.enableSwipeToSeeTime`
/// is `true`. When set, this takes priority over
/// [MessageListConfiguration.messageTimeTextStyle] for individual bubbles.
final TextStyle? messageTimeTextStyle;

/// Used for giving padding of chat bubble.
final EdgeInsetsGeometry? padding;

Expand Down
12 changes: 12 additions & 0 deletions lib/src/models/config_models/feature_active_config.dart
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we give true both to enableSwipeToReply enableSwipeToSeeTime?

Copy link
Copy Markdown
Contributor Author

@vasu-nageshri vasu-nageshri May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

swipe-to-reply will work when swiping directly on the message bubble. Swiping outside the bubble will continue to show the message time UI, so there won’t be any conflict between the two gestures.

Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class FeatureActiveConfig {
this.enableReactionPopup = true,
this.enableTextField = true,
this.enableSwipeToSeeTime = true,
this.showTimestamp = false,
this.enableCurrentUserProfileAvatar = false,
this.enableOtherUserProfileAvatar = true,
this.enableReplySnackBar = true,
Expand All @@ -51,6 +52,17 @@ class FeatureActiveConfig {
/// Used for enable/disable swipe whole chat to see message created time.
final bool enableSwipeToSeeTime;

/// Used to globally control whether message timestamps are shown inside chat bubbles.
/// Defaults to `false`.
///
/// When `true`, timestamps are displayed inside bubbles for all message types
/// (text, image, voice). Use `ChatBubble.messageTimeTextStyle` to customise
/// the appearance of the timestamp text per bubble direction.
///
/// See also: [enableSwipeToSeeTime], which uses the same `ChatBubble.messageTimeTextStyle`
/// for its per-bubble swipe-out timestamp styling.
final bool showTimestamp;

/// Used for enable/disable current user profile circle.
final bool enableCurrentUserProfileAvatar;

Expand Down
4 changes: 4 additions & 0 deletions lib/src/models/config_models/message_list_configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ class ChatBackgroundConfiguration {

/// Used to give text style of message's time while user swipe to see time of
/// message.
///
/// Acts as a global fallback for the swipe-to-reveal timestamp.
/// If [ChatBubble.messageTimeTextStyle] is set on the outgoing or incoming
/// bubble config, it takes priority over this value for that specific bubble.
final TextStyle? messageTimeTextStyle;

/// Used to give colour of message's time while user swipe to see time of
Expand Down
29 changes: 22 additions & 7 deletions lib/src/widgets/chat_bubble_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,20 @@ class _ChatBubbleWidgetState extends State<ChatBubbleWidget> {
Widget build(BuildContext context) {
// Get user from id.
final messagedUser = chatController?.getUserFromId(widget.message.sentBy);
return Stack(
children: [
if (featureActiveConfig?.enableSwipeToSeeTime ?? true) ...[

// showTimestamp and enableSwipeToSeeTime are mutually exclusive
// (enforced by FeatureActiveConfig's assert). Only one can ever be true.
final bool showTimestamp = featureActiveConfig?.showTimestamp ?? false;

// Use swipe-to-see-time only when in-bubble timestamps are not active.
final bool useSwipeToSeeTime =
!showTimestamp && (featureActiveConfig?.enableSwipeToSeeTime ?? true);

if (useSwipeToSeeTime && widget.slideAnimation != null) {
return Stack(
children: [
Visibility(
visible: widget.slideAnimation?.value.dx == 0.0 ? false : true,
visible: widget.slideAnimation!.value.dx != 0.0,
child: Positioned.fill(
child: Align(
alignment: Alignment.centerRight,
Expand All @@ -113,9 +122,15 @@ class _ChatBubbleWidgetState extends State<ChatBubbleWidget> {
position: widget.slideAnimation!,
child: _chatBubbleWidget(messagedUser),
),
] else
_chatBubbleWidget(messagedUser),
],
],
);
}

final slideAnimation =
widget.slideAnimation ?? const AlwaysStoppedAnimation(Offset.zero);
return SlideTransition(
position: slideAnimation,
child: _chatBubbleWidget(messagedUser),
);
}

Expand Down
14 changes: 12 additions & 2 deletions lib/src/widgets/chat_list_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ class _ChatListWidgetState extends State<ChatListWidget> {
bool get isPaginationEnabled =>
featureActiveConfig?.enablePagination ?? false;

/// Returns true when the swipe-to-see-time gesture should be active.
///
/// [FeatureActiveConfig.showTimestamp] and [FeatureActiveConfig.enableSwipeToSeeTime]
/// are mutually exclusive — the [FeatureActiveConfig] assert already prevents
/// both being true simultaneously.
bool get _resolveSwipeToSeeTime {
final showTimestamp = featureActiveConfig?.showTimestamp ?? false;
return !showTimestamp &&
(featureActiveConfig?.enableSwipeToSeeTime ?? true);
}

@override
void initState() {
super.initState();
Expand All @@ -106,8 +117,7 @@ class _ChatListWidgetState extends State<ChatListWidget> {
loadingWidget: widget.loadingWidget,
showPopUp: showPopupValue,
scrollController: scrollController,
isEnableSwipeToSeeTime:
featureActiveConfig?.enableSwipeToSeeTime ?? true,
isEnableSwipeToSeeTime: _resolveSwipeToSeeTime,
assignReplyMessage: widget.assignReplyMessage,
onChatListTap: _onChatListTap,
textFieldConfig: widget.textFieldConfig,
Expand Down
116 changes: 78 additions & 38 deletions lib/src/widgets/image_message_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import 'package:flutter/material.dart';

import '../extensions/extensions.dart';
import '../models/chat_bubble.dart';
import '../models/config_models/feature_active_config.dart';
import '../models/config_models/image_message_configuration.dart';
import '../models/config_models/message_reaction_configuration.dart';
import 'reaction_widget.dart';
Expand All @@ -44,6 +45,7 @@ class ImageMessageView extends StatelessWidget {
this.outgoingChatBubbleConfig,
this.highlightImage = false,
this.highlightScale = 1.2,
this.featureActiveConfig,
}) : super(key: key);

/// Provides configuration of chat bubble appearance from other user of chat.
Expand All @@ -70,6 +72,9 @@ class ImageMessageView extends StatelessWidget {
/// Provides scale of highlighted image when user taps on replied image.
final double highlightScale;

/// Provides configuration of active features in chat.
final FeatureActiveConfig? featureActiveConfig;

String get imageUrl => message.message;

Widget get iconButton => ShareIcon(
Expand All @@ -80,7 +85,7 @@ class ImageMessageView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final borderRadius = imageMessageConfig?.borderRadius ??
const BorderRadius.all(Radius.circular(14));
const BorderRadius.all(Radius.circular(10));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the Bubble UI looking good without timestamp also?

final backgroundColor = isMessageBySender
? outgoingChatBubbleConfig?.color ?? Colors.purple
: inComingChatBubbleConfig?.color ?? Colors.grey.shade500;
Expand Down Expand Up @@ -118,44 +123,79 @@ class ImageMessageView extends StatelessWidget {
),
height: imageMessageConfig?.height ?? 200,
width: imageMessageConfig?.width ?? 150,
child: ClipRRect(
borderRadius: borderRadius,
child: (() {
if (imageUrl.isUrl) {
return Image.network(
imageUrl,
fit: BoxFit.fitHeight,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes !=
null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
child: Stack(
children: [
ClipRRect(
borderRadius: borderRadius,
child: (() {
if (imageUrl.isUrl) {
return Image.network(
imageUrl,
fit: BoxFit.fitHeight,
loadingBuilder:
(context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
);
},
);
} else if (imageUrl.fromMemory) {
return Image.memory(
base64Decode(imageUrl
.substring(imageUrl.indexOf('base64') + 7)),
fit: BoxFit.fill,
);
} else {
return kIsWeb
? Image.network(
imageUrl,
fit: BoxFit.fill,
)
: Image.file(
File(imageUrl),
fit: BoxFit.fill,
);
}
}()),
} else if (imageUrl.fromMemory) {
return Image.memory(
base64Decode(imageUrl
.substring(imageUrl.indexOf('base64') + 7)),
fit: BoxFit.fill,
);
} else {
return kIsWeb
? Image.network(
imageUrl,
fit: BoxFit.fill,
)
: Image.file(
File(imageUrl),
fit: BoxFit.fill,
);
}
}()),
),
if (featureActiveConfig?.showTimestamp ?? false)
Positioned(
right: 8,
bottom: 8,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.black45,
borderRadius: BorderRadius.circular(10),
),
child: Text(
message.createdAt.getTimeFromDateTime,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w500,
).merge(
isMessageBySender
? outgoingChatBubbleConfig
?.messageTimeTextStyle
: inComingChatBubbleConfig
?.messageTimeTextStyle,
),
),
),
),
],
),
),
),
Expand Down
2 changes: 2 additions & 0 deletions lib/src/widgets/message_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ class _MessageViewState extends State<MessageView>
outgoingChatBubbleConfig: widget.outgoingChatBubbleConfig,
highlightImage: widget.shouldHighlight,
highlightScale: widget.highlightScale,
featureActiveConfig: chatViewIW?.featureActiveConfig,
);
} else if (widget.message.messageType.isText) {
return TextMessageView(
Expand All @@ -243,6 +244,7 @@ class _MessageViewState extends State<MessageView>
messageReactionConfig: messageConfig?.messageReactionConfig,
inComingChatBubbleConfig: widget.inComingChatBubbleConfig,
outgoingChatBubbleConfig: widget.outgoingChatBubbleConfig,
featureActiveConfig: chatViewIW?.featureActiveConfig,
);
} else if (widget.message.messageType.isCustom &&
messageConfig?.customMessageBuilder != null) {
Expand Down
Loading
Loading