Skip to content

Commit ef1f4ff

Browse files
committed
feat: ✨ Add customizable timestamp styling for chat bubbles and improve timestamp rendering logic
- Adaptive layout: inline for short messages, bottom-right for long messages - Image/voice message timestamps overlaid inside the bubble - Add in for per-bubble timestamp styling - Update time format to 12-hour AM/PM (e.g. 04:32 PM) - Add voice message duration display: (VoiceDurationFormat.hhmmss/mmss/adaptive) - Reduce chat bubble border radius for modern look
1 parent 5385ee5 commit ef1f4ff

11 files changed

Lines changed: 293 additions & 62 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Rendering issue in attached image preview when sending message on web.
55
* **Feat**: [420](https://github.com/SimformSolutionsPvtLtd/chatview/pull/420) Added support for
66
`playerMode` in `VoiceMessageConfiguration` with `single` and `multi`.
7+
* **Feat**: [115](https://github.com/SimformSolutionsPvtLtd/chatview/issues/115) Add customizable timestamp styling for chat bubbles and improve timestamp rendering logic.
78

89
## [3.0.0]
910

doc/documentation.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,12 @@ ChatView(
728728
topLeft: Radius.circular(12),
729729
bottomLeft: Radius.circular(12),
730730
),
731+
// Style for in-bubble timestamp text (e.g. "04:32 PM").
732+
messageTimeTextStyle: const TextStyle(
733+
color: Colors.white70,
734+
fontSize: 11,
735+
fontWeight: FontWeight.w500,
736+
),
731737
),
732738
inComingChatBubbleConfig: ChatBubble(
733739
color: Colors.grey.shade200,
@@ -736,12 +742,25 @@ ChatView(
736742
topRight: Radius.circular(12),
737743
bottomRight: Radius.circular(12),
738744
),
745+
// Style for in-bubble timestamp text (e.g. "04:32 PM").
746+
messageTimeTextStyle: const TextStyle(
747+
color: Colors.black54,
748+
fontSize: 11,
749+
fontWeight: FontWeight.w500,
750+
),
739751
),
740752
),
741753
// ...
742754
)
743755
```
744756

757+
Timestamps rendered in bubbles use 12-hour format with AM/PM (for example, `04:32 PM`).
758+
You can control timestamp typography per bubble side using `messageTimeTextStyle`.
759+
760+
> **Note:** `showTimestamp` (in-bubble timestamp) and `enableSwipeToSeeTime` (swipe-to-reveal timestamp) are mutually exclusive.
761+
> Both live in `FeatureActiveConfig`. Setting both to `true` raises an `AssertionError` in debug mode.
762+
> Use `showTimestamp: true` to display the time inside each bubble, or `enableSwipeToSeeTime: true` to reveal it on swipe — never both.
763+
745764
## Swipe to Reply Configuration
746765

747766
```dart
@@ -847,7 +866,10 @@ ChatView(
847866
// ...
848867
featureActiveConfig: FeatureActiveConfig(
849868
enableSwipeToReply: true,
850-
enableSwipeToSeeTime: false,
869+
// Set to true to reveal message time by swiping the chat bubble.
870+
// Mutually exclusive with showTimestamp — setting both true raises
871+
// an AssertionError in debug mode.
872+
enableSwipeToSeeTime: true,
851873
enablePagination: true,
852874
enableOtherUserName: false,
853875
lastSeenAgoBuilderVisibility: false,

lib/src/extensions/extensions.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ extension TimeDifference on DateTime {
7070
return formatter.format(this);
7171
}
7272

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

7575
/// Returns `true` if [other] occurs on the same calendar day as
7676
/// this [DateTime].

lib/src/models/chat_bubble.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class ChatBubble {
3131
this.color,
3232
this.borderRadius,
3333
this.textStyle,
34+
this.messageTimeTextStyle,
3435
this.padding,
3536
this.margin,
3637
this.linkPreviewConfig,
@@ -50,6 +51,10 @@ class ChatBubble {
5051
/// Used for giving text style of chat bubble.
5152
final TextStyle? textStyle;
5253

54+
/// Used for giving text style of in-bubble message timestamp.
55+
/// Only has effect when [FeatureActiveConfig.showTimestamp] is `true`.
56+
final TextStyle? messageTimeTextStyle;
57+
5358
/// Used for giving padding of chat bubble.
5459
final EdgeInsetsGeometry? padding;
5560

lib/src/models/config_models/feature_active_config.dart

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class FeatureActiveConfig {
2626
this.enableReactionPopup = true,
2727
this.enableTextField = true,
2828
this.enableSwipeToSeeTime = true,
29+
this.showTimestamp = false,
2930
this.enableCurrentUserProfileAvatar = false,
3031
this.enableOtherUserProfileAvatar = true,
3132
this.enableReplySnackBar = true,
@@ -37,7 +38,13 @@ class FeatureActiveConfig {
3738
this.enableOtherUserName = true,
3839
this.enableScrollToBottomButton = false,
3940
this.enableTextSelection = false,
40-
});
41+
}) : assert(
42+
!(enableSwipeToSeeTime && showTimestamp),
43+
'FeatureActiveConfig: enableSwipeToSeeTime and showTimestamp cannot '
44+
'both be true at the same time. '
45+
'Use showTimestamp: true to display the time inside each bubble, '
46+
'or enableSwipeToSeeTime: true to reveal it on swipe — not both.',
47+
);
4148

4249
/// Used for enable/disable swipe to reply.
4350
final bool enableSwipeToReply;
@@ -49,8 +56,24 @@ class FeatureActiveConfig {
4956
final bool enableTextField;
5057

5158
/// Used for enable/disable swipe whole chat to see message created time.
59+
///
60+
/// **Mutually exclusive with `showTimestamp`.**
61+
/// Setting both `enableSwipeToSeeTime: true` and `showTimestamp: true` will
62+
/// throw an [AssertionError] in debug mode.
5263
final bool enableSwipeToSeeTime;
5364

65+
/// Used to globally control whether message timestamps are shown inside chat bubbles.
66+
/// Defaults to `false`.
67+
///
68+
/// **Mutually exclusive with `enableSwipeToSeeTime`.**
69+
/// Setting both `showTimestamp: true` and `enableSwipeToSeeTime: true` will
70+
/// throw an [AssertionError] in debug mode.
71+
///
72+
/// When `true`, timestamps are displayed inside bubbles for all message types
73+
/// (text, image, voice). Use [ChatBubble.messageTimeTextStyle] to customise
74+
/// the appearance of the timestamp text per bubble direction.
75+
final bool showTimestamp;
76+
5477
/// Used for enable/disable current user profile circle.
5578
final bool enableCurrentUserProfileAvatar;
5679

lib/src/widgets/chat_bubble_widget.dart

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,20 @@ class _ChatBubbleWidgetState extends State<ChatBubbleWidget> {
9494
Widget build(BuildContext context) {
9595
// Get user from id.
9696
final messagedUser = chatController?.getUserFromId(widget.message.sentBy);
97-
return Stack(
98-
children: [
99-
if (featureActiveConfig?.enableSwipeToSeeTime ?? true) ...[
97+
98+
// showTimestamp and enableSwipeToSeeTime are mutually exclusive
99+
// (enforced by FeatureActiveConfig's assert). Only one can ever be true.
100+
final bool showTimestamp = featureActiveConfig?.showTimestamp ?? false;
101+
102+
// Use swipe-to-see-time only when in-bubble timestamps are not active.
103+
final bool useSwipeToSeeTime =
104+
!showTimestamp && (featureActiveConfig?.enableSwipeToSeeTime ?? true);
105+
106+
if (useSwipeToSeeTime && widget.slideAnimation != null) {
107+
return Stack(
108+
children: [
100109
Visibility(
101-
visible: widget.slideAnimation?.value.dx == 0.0 ? false : true,
110+
visible: widget.slideAnimation!.value.dx != 0.0,
102111
child: Positioned.fill(
103112
child: Align(
104113
alignment: Alignment.centerRight,
@@ -113,9 +122,15 @@ class _ChatBubbleWidgetState extends State<ChatBubbleWidget> {
113122
position: widget.slideAnimation!,
114123
child: _chatBubbleWidget(messagedUser),
115124
),
116-
] else
117-
_chatBubbleWidget(messagedUser),
118-
],
125+
],
126+
);
127+
}
128+
129+
final slideAnimation =
130+
widget.slideAnimation ?? const AlwaysStoppedAnimation(Offset.zero);
131+
return SlideTransition(
132+
position: slideAnimation,
133+
child: _chatBubbleWidget(messagedUser),
119134
);
120135
}
121136

lib/src/widgets/chat_list_widget.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ class _ChatListWidgetState extends State<ChatListWidget> {
8181
bool get isPaginationEnabled =>
8282
featureActiveConfig?.enablePagination ?? false;
8383

84+
/// Returns true when the swipe-to-see-time gesture should be active.
85+
///
86+
/// [FeatureActiveConfig.showTimestamp] and [FeatureActiveConfig.enableSwipeToSeeTime]
87+
/// are mutually exclusive — the [FeatureActiveConfig] assert already prevents
88+
/// both being true simultaneously.
89+
bool get _resolveSwipeToSeeTime {
90+
final showTimestamp = featureActiveConfig?.showTimestamp ?? false;
91+
return !showTimestamp && (featureActiveConfig?.enableSwipeToSeeTime ?? true);
92+
}
93+
8494
@override
8595
void initState() {
8696
super.initState();
@@ -106,8 +116,7 @@ class _ChatListWidgetState extends State<ChatListWidget> {
106116
loadingWidget: widget.loadingWidget,
107117
showPopUp: showPopupValue,
108118
scrollController: scrollController,
109-
isEnableSwipeToSeeTime:
110-
featureActiveConfig?.enableSwipeToSeeTime ?? true,
119+
isEnableSwipeToSeeTime: _resolveSwipeToSeeTime,
111120
assignReplyMessage: widget.assignReplyMessage,
112121
onChatListTap: _onChatListTap,
113122
textFieldConfig: widget.textFieldConfig,

lib/src/widgets/image_message_view.dart

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import 'package:flutter/material.dart';
2828

2929
import '../extensions/extensions.dart';
3030
import '../models/chat_bubble.dart';
31+
import '../models/config_models/feature_active_config.dart';
3132
import '../models/config_models/image_message_configuration.dart';
3233
import '../models/config_models/message_reaction_configuration.dart';
3334
import 'reaction_widget.dart';
@@ -44,6 +45,7 @@ class ImageMessageView extends StatelessWidget {
4445
this.outgoingChatBubbleConfig,
4546
this.highlightImage = false,
4647
this.highlightScale = 1.2,
48+
this.featureActiveConfig,
4749
}) : super(key: key);
4850

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

75+
/// Provides configuration of active features in chat.
76+
final FeatureActiveConfig? featureActiveConfig;
77+
7378
String get imageUrl => message.message;
7479

7580
Widget get iconButton => ShareIcon(
@@ -80,7 +85,7 @@ class ImageMessageView extends StatelessWidget {
8085
@override
8186
Widget build(BuildContext context) {
8287
final borderRadius = imageMessageConfig?.borderRadius ??
83-
const BorderRadius.all(Radius.circular(14));
88+
const BorderRadius.all(Radius.circular(10));
8489
final backgroundColor = isMessageBySender
8590
? outgoingChatBubbleConfig?.color ?? Colors.purple
8691
: inComingChatBubbleConfig?.color ?? Colors.grey.shade500;
@@ -118,44 +123,75 @@ class ImageMessageView extends StatelessWidget {
118123
),
119124
height: imageMessageConfig?.height ?? 200,
120125
width: imageMessageConfig?.width ?? 150,
121-
child: ClipRRect(
122-
borderRadius: borderRadius,
123-
child: (() {
124-
if (imageUrl.isUrl) {
125-
return Image.network(
126-
imageUrl,
127-
fit: BoxFit.fitHeight,
128-
loadingBuilder: (context, child, loadingProgress) {
129-
if (loadingProgress == null) return child;
130-
return Center(
131-
child: CircularProgressIndicator(
132-
value: loadingProgress.expectedTotalBytes !=
133-
null
134-
? loadingProgress.cumulativeBytesLoaded /
135-
loadingProgress.expectedTotalBytes!
136-
: null,
137-
),
126+
child: Stack(
127+
children: [
128+
ClipRRect(
129+
borderRadius: borderRadius,
130+
child: (() {
131+
if (imageUrl.isUrl) {
132+
return Image.network(
133+
imageUrl,
134+
fit: BoxFit.fitHeight,
135+
loadingBuilder: (context, child, loadingProgress) {
136+
if (loadingProgress == null) return child;
137+
return Center(
138+
child: CircularProgressIndicator(
139+
value: loadingProgress.expectedTotalBytes !=
140+
null
141+
? loadingProgress.cumulativeBytesLoaded /
142+
loadingProgress.expectedTotalBytes!
143+
: null,
144+
),
145+
);
146+
},
147+
);
148+
} else if (imageUrl.fromMemory) {
149+
return Image.memory(
150+
base64Decode(imageUrl
151+
.substring(imageUrl.indexOf('base64') + 7)),
152+
fit: BoxFit.fill,
138153
);
139-
},
140-
);
141-
} else if (imageUrl.fromMemory) {
142-
return Image.memory(
143-
base64Decode(imageUrl
144-
.substring(imageUrl.indexOf('base64') + 7)),
145-
fit: BoxFit.fill,
146-
);
147-
} else {
148-
return kIsWeb
149-
? Image.network(
150-
imageUrl,
151-
fit: BoxFit.fill,
152-
)
153-
: Image.file(
154-
File(imageUrl),
155-
fit: BoxFit.fill,
156-
);
157-
}
158-
}()),
154+
} else {
155+
return kIsWeb
156+
? Image.network(
157+
imageUrl,
158+
fit: BoxFit.fill,
159+
)
160+
: Image.file(
161+
File(imageUrl),
162+
fit: BoxFit.fill,
163+
);
164+
}
165+
}()),
166+
),
167+
if (featureActiveConfig?.showTimestamp ?? false)
168+
Positioned(
169+
right: 8,
170+
bottom: 8,
171+
child: Container(
172+
padding: const EdgeInsets.symmetric(
173+
horizontal: 6,
174+
vertical: 2,
175+
),
176+
decoration: BoxDecoration(
177+
color: Colors.black45,
178+
borderRadius: BorderRadius.circular(10),
179+
),
180+
child: Text(
181+
message.createdAt.getTimeFromDateTime,
182+
style: const TextStyle(
183+
color: Colors.white,
184+
fontSize: 10,
185+
fontWeight: FontWeight.w500,
186+
).merge(
187+
isMessageBySender
188+
? outgoingChatBubbleConfig?.messageTimeTextStyle
189+
: inComingChatBubbleConfig?.messageTimeTextStyle,
190+
),
191+
),
192+
),
193+
),
194+
],
159195
),
160196
),
161197
),

lib/src/widgets/message_view.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ class _MessageViewState extends State<MessageView>
220220
outgoingChatBubbleConfig: widget.outgoingChatBubbleConfig,
221221
highlightImage: widget.shouldHighlight,
222222
highlightScale: widget.highlightScale,
223+
featureActiveConfig: chatViewIW?.featureActiveConfig,
223224
);
224225
} else if (widget.message.messageType.isText) {
225226
return TextMessageView(
@@ -243,6 +244,7 @@ class _MessageViewState extends State<MessageView>
243244
messageReactionConfig: messageConfig?.messageReactionConfig,
244245
inComingChatBubbleConfig: widget.inComingChatBubbleConfig,
245246
outgoingChatBubbleConfig: widget.outgoingChatBubbleConfig,
247+
featureActiveConfig: chatViewIW?.featureActiveConfig,
246248
);
247249
} else if (widget.message.messageType.isCustom &&
248250
messageConfig?.customMessageBuilder != null) {

0 commit comments

Comments
 (0)