Skip to content

Commit 3899379

Browse files
authored
feat(ui): Add date formatters for timestamps (#2438)
1 parent f9d2116 commit 3899379

File tree

20 files changed

+586
-18
lines changed

20 files changed

+586
-18
lines changed

packages/stream_chat_flutter/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
## Upcoming
22

3+
✅ Added
4+
5+
- Added formatter properties to theme data classes for customizing date/timestamp
6+
formatting. [[#2312]](https://github.com/GetStream/stream-chat-flutter/issues/2312) [[#2406]](https://github.com/GetStream/stream-chat-flutter/issues/2406)
7+
38
🐞 Fixed
49

510
- Fixed mistakenly passing the hyperlink text to the `onLinkTap` callback instead of the actual `href`.

packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,13 @@ class BottomRow extends StatelessWidget {
215215
StreamTimestamp(
216216
date: message.createdAt.toLocal(),
217217
style: messageTheme.createdAtStyle,
218-
formatter: (_, date) => Jiffy.parseFromDateTime(date).jm,
218+
formatter: (context, date) {
219+
if (messageTheme.createdAtFormatter case final formatter?) {
220+
return formatter.call(context, date);
221+
}
222+
223+
return Jiffy.parseFromDateTime(date).jm;
224+
},
219225
),
220226
];
221227

packages/stream_chat_flutter/lib/src/misc/date_divider.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class StreamDateDivider extends StatelessWidget {
1212
super.key,
1313
required this.dateTime,
1414
this.uppercase = false,
15+
this.formatter,
1516
});
1617

1718
/// [DateTime] to display
@@ -20,6 +21,9 @@ class StreamDateDivider extends StatelessWidget {
2021
/// If text is uppercase
2122
final bool uppercase;
2223

24+
/// Custom formatter for the date
25+
final DateFormatter? formatter;
26+
2327
@override
2428
Widget build(BuildContext context) {
2529
final chatThemeData = StreamChatTheme.of(context);
@@ -36,6 +40,12 @@ class StreamDateDivider extends StatelessWidget {
3640
color: chatThemeData.colorTheme.barsBg,
3741
),
3842
formatter: (context, date) {
43+
if (formatter case final formatter?) {
44+
final timestamp = formatter.call(context, date);
45+
if (uppercase) return timestamp.toUpperCase();
46+
return timestamp;
47+
}
48+
3949
final timestamp = switch (date) {
4050
_ when date.isToday => context.translations.todayLabel,
4151
_ when date.isYesterday => context.translations.yesterdayLabel,

packages/stream_chat_flutter/lib/src/misc/timestamp.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ class StreamTimestamp extends StatelessWidget {
1212
const StreamTimestamp({
1313
super.key,
1414
required this.date,
15-
this.formatter = formatDate,
15+
DateFormatter? formatter,
1616
this.style,
1717
this.textAlign,
1818
this.textDirection,
19-
});
19+
}) : formatter = formatter ?? formatDate;
2020

2121
/// The date to show in the timestamp.
2222
final DateTime date;

packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_tile.dart

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:stream_chat_flutter/src/channel/stream_draft_message_preview_tex
55
import 'package:stream_chat_flutter/src/message_widget/sending_indicator_builder.dart';
66
import 'package:stream_chat_flutter/src/misc/empty_widget.dart';
77
import 'package:stream_chat_flutter/src/misc/timestamp.dart';
8+
import 'package:stream_chat_flutter/src/utils/date_formatter.dart';
89
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
910

1011
/// A widget that displays a channel preview.
@@ -174,6 +175,7 @@ class StreamChannelListTile extends StatelessWidget {
174175
ChannelLastMessageDate(
175176
channel: channel,
176177
textStyle: channelPreviewTheme.lastMessageAtStyle,
178+
formatter: channelPreviewTheme.lastMessageAtFormatter,
177179
);
178180

179181
return BetterStreamBuilder<bool>(
@@ -265,6 +267,7 @@ class ChannelLastMessageDate extends StatelessWidget {
265267
super.key,
266268
required this.channel,
267269
this.textStyle,
270+
this.formatter,
268271
}) : assert(
269272
channel.state != null,
270273
'Channel ${channel.id} is not initialized',
@@ -276,17 +279,21 @@ class ChannelLastMessageDate extends StatelessWidget {
276279
/// The style of the text displayed
277280
final TextStyle? textStyle;
278281

282+
/// The formatter to format the date.
283+
final DateFormatter? formatter;
284+
279285
@override
280-
Widget build(BuildContext context) => BetterStreamBuilder<DateTime>(
281-
stream: channel.lastMessageAtStream,
282-
initialData: channel.lastMessageAt,
283-
builder: (context, lastMessageAt) {
284-
return StreamTimestamp(
285-
date: lastMessageAt.toLocal(),
286-
style: textStyle,
287-
);
288-
},
289-
);
286+
Widget build(BuildContext context) {
287+
return BetterStreamBuilder<DateTime>(
288+
stream: channel.lastMessageAtStream,
289+
initialData: channel.lastMessageAt,
290+
builder: (context, lastMessageAt) => StreamTimestamp(
291+
date: lastMessageAt.toLocal(),
292+
style: textStyle,
293+
formatter: formatter,
294+
),
295+
);
296+
}
290297
}
291298

292299
/// A widget that displays the subtitle for [StreamChannelListTile].

packages/stream_chat_flutter/lib/src/scroll_view/draft_scroll_view/stream_draft_list_tile.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ class DraftMessageContent extends StatelessWidget {
136136
StreamTimestamp(
137137
date: draft.createdAt.toLocal(),
138138
style: theme.draftTimestampStyle,
139+
formatter: theme.draftTimestampFormatter,
139140
),
140141
],
141142
);

packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:stream_chat_flutter/src/misc/timestamp.dart';
3+
import 'package:stream_chat_flutter/src/utils/date_formatter.dart';
34
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
45

56
/// A widget that displays a message search item.
@@ -140,6 +141,7 @@ class StreamMessageSearchListTile extends StatelessWidget {
140141
MessageSearchTileMessageDate(
141142
message: message,
142143
textStyle: channelPreviewTheme.lastMessageAtStyle,
144+
formatter: channelPreviewTheme.lastMessageAtFormatter,
143145
),
144146
],
145147
);
@@ -212,6 +214,7 @@ class MessageSearchTileMessageDate extends StatelessWidget {
212214
super.key,
213215
required this.message,
214216
this.textStyle,
217+
this.formatter,
215218
});
216219

217220
/// The searched message response.
@@ -220,12 +223,16 @@ class MessageSearchTileMessageDate extends StatelessWidget {
220223
/// The text style to use for the date.
221224
final TextStyle? textStyle;
222225

226+
/// An optional formatter to format the date.
227+
final DateFormatter? formatter;
228+
223229
@override
224230
Widget build(BuildContext context) {
225231
final createdAt = message.createdAt;
226232
return StreamTimestamp(
227233
date: createdAt.toLocal(),
228234
style: textStyle,
235+
formatter: formatter,
229236
);
230237
}
231238
}

packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_tile.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ class ThreadLatestReply extends StatelessWidget {
260260
StreamTimestamp(
261261
date: latestReply.createdAt.toLocal(),
262262
style: theme.threadLatestReplyTimestampStyle,
263+
formatter: theme.threadLatestReplyTimestampFormatter,
263264
),
264265
],
265266
),

packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
22
import 'package:flutter/material.dart';
33
import 'package:stream_chat_flutter/src/theme/avatar_theme.dart';
44
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
5+
import 'package:stream_chat_flutter/src/utils/date_formatter.dart';
56

67
/// {@template channelPreviewTheme}
78
/// Overrides the default style of [ChannelPreview] descendants.
@@ -70,6 +71,7 @@ class StreamChannelPreviewThemeData with Diagnosticable {
7071
this.avatarTheme,
7172
this.unreadCounterColor,
7273
this.indicatorIconSize,
74+
this.lastMessageAtFormatter,
7375
});
7476

7577
/// Theme for title
@@ -90,6 +92,21 @@ class StreamChannelPreviewThemeData with Diagnosticable {
9092
/// Indicator icon size
9193
final double? indicatorIconSize;
9294

95+
/// Formatter for the last message timestamp.
96+
///
97+
/// If null, uses the default date formatting.
98+
///
99+
/// Example:
100+
/// ```dart
101+
/// StreamChannelPreviewThemeData(
102+
/// lastMessageAtStyle: TextStyle(...),
103+
/// lastMessageAtFormatter: (context, date) {
104+
/// return Jiffy.parseFromDateTime(date).format('d MMMM'); // "23 May"
105+
/// },
106+
/// )
107+
/// ```
108+
final DateFormatter? lastMessageAtFormatter;
109+
93110
/// Copy with theme
94111
StreamChannelPreviewThemeData copyWith({
95112
TextStyle? titleStyle,
@@ -98,6 +115,7 @@ class StreamChannelPreviewThemeData with Diagnosticable {
98115
StreamAvatarThemeData? avatarTheme,
99116
Color? unreadCounterColor,
100117
double? indicatorIconSize,
118+
DateFormatter? lastMessageAtFormatter,
101119
}) {
102120
return StreamChannelPreviewThemeData(
103121
titleStyle: titleStyle ?? this.titleStyle,
@@ -106,6 +124,8 @@ class StreamChannelPreviewThemeData with Diagnosticable {
106124
avatarTheme: avatarTheme ?? this.avatarTheme,
107125
unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor,
108126
indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize,
127+
lastMessageAtFormatter:
128+
lastMessageAtFormatter ?? this.lastMessageAtFormatter,
109129
);
110130
}
111131

@@ -125,6 +145,8 @@ class StreamChannelPreviewThemeData with Diagnosticable {
125145
titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t),
126146
unreadCounterColor:
127147
Color.lerp(a.unreadCounterColor, b.unreadCounterColor, t),
148+
lastMessageAtFormatter:
149+
t < 0.5 ? a.lastMessageAtFormatter : b.lastMessageAtFormatter,
128150
);
129151
}
130152

@@ -139,6 +161,8 @@ class StreamChannelPreviewThemeData with Diagnosticable {
139161
other.lastMessageAtStyle,
140162
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
141163
unreadCounterColor: other.unreadCounterColor,
164+
lastMessageAtFormatter:
165+
other.lastMessageAtFormatter ?? lastMessageAtFormatter,
142166
);
143167
}
144168

@@ -152,7 +176,8 @@ class StreamChannelPreviewThemeData with Diagnosticable {
152176
lastMessageAtStyle == other.lastMessageAtStyle &&
153177
avatarTheme == other.avatarTheme &&
154178
unreadCounterColor == other.unreadCounterColor &&
155-
indicatorIconSize == other.indicatorIconSize;
179+
indicatorIconSize == other.indicatorIconSize &&
180+
lastMessageAtFormatter == other.lastMessageAtFormatter;
156181

157182
@override
158183
int get hashCode =>
@@ -161,7 +186,8 @@ class StreamChannelPreviewThemeData with Diagnosticable {
161186
lastMessageAtStyle.hashCode ^
162187
avatarTheme.hashCode ^
163188
unreadCounterColor.hashCode ^
164-
indicatorIconSize.hashCode;
189+
indicatorIconSize.hashCode ^
190+
lastMessageAtFormatter.hashCode;
165191

166192
@override
167193
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@@ -171,6 +197,8 @@ class StreamChannelPreviewThemeData with Diagnosticable {
171197
..add(DiagnosticsProperty('subtitleStyle', subtitleStyle))
172198
..add(DiagnosticsProperty('lastMessageAtStyle', lastMessageAtStyle))
173199
..add(DiagnosticsProperty('avatarTheme', avatarTheme))
174-
..add(ColorProperty('unreadCounterColor', unreadCounterColor));
200+
..add(ColorProperty('unreadCounterColor', unreadCounterColor))
201+
..add(DiagnosticsProperty(
202+
'lastMessageAtFormatter', lastMessageAtFormatter));
175203
}
176204
}

packages/stream_chat_flutter/lib/src/theme/draft_list_tile_theme.dart

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter/foundation.dart';
22
import 'package:flutter/widgets.dart';
33
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
4+
import 'package:stream_chat_flutter/src/utils/date_formatter.dart';
45

56
/// {@template streamDraftListTileTheme}
67
/// Overrides the default style of [StreamDraftListTile] descendants.
@@ -57,6 +58,7 @@ class StreamDraftListTileThemeData with Diagnosticable {
5758
this.draftChannelNameStyle,
5859
this.draftMessageStyle,
5960
this.draftTimestampStyle,
61+
this.draftTimestampFormatter,
6062
});
6163

6264
/// The padding around the [StreamDraftListTile] widget.
@@ -74,6 +76,21 @@ class StreamDraftListTileThemeData with Diagnosticable {
7476
/// The style of the draft timestamp in the [StreamDraftListTile] widget.
7577
final TextStyle? draftTimestampStyle;
7678

79+
/// Formatter for the draft timestamp.
80+
///
81+
/// If null, uses the default date formatting.
82+
///
83+
/// Example:
84+
/// ```dart
85+
/// StreamDraftListTileThemeData(
86+
/// draftTimestampStyle: TextStyle(...),
87+
/// draftTimestampFormatter: (context, date) {
88+
/// return Jiffy.parseFromDateTime(date).fromNow(); // "2 hours ago"
89+
/// },
90+
/// )
91+
/// ```
92+
final DateFormatter? draftTimestampFormatter;
93+
7794
/// A copy of [StreamDraftListTileThemeData] with specified attributes
7895
/// overridden.
7996
StreamDraftListTileThemeData copyWith({
@@ -82,6 +99,7 @@ class StreamDraftListTileThemeData with Diagnosticable {
8299
TextStyle? draftChannelNameStyle,
83100
TextStyle? draftMessageStyle,
84101
TextStyle? draftTimestampStyle,
102+
DateFormatter? draftTimestampFormatter,
85103
Color? draftIconColor,
86104
}) =>
87105
StreamDraftListTileThemeData(
@@ -91,6 +109,8 @@ class StreamDraftListTileThemeData with Diagnosticable {
91109
draftChannelNameStyle ?? this.draftChannelNameStyle,
92110
draftMessageStyle: draftMessageStyle ?? this.draftMessageStyle,
93111
draftTimestampStyle: draftTimestampStyle ?? this.draftTimestampStyle,
112+
draftTimestampFormatter:
113+
draftTimestampFormatter ?? this.draftTimestampFormatter,
94114
);
95115

96116
/// Merges this [StreamDraftListTileThemeData] with the [other].
@@ -104,6 +124,7 @@ class StreamDraftListTileThemeData with Diagnosticable {
104124
draftChannelNameStyle: other.draftChannelNameStyle,
105125
draftMessageStyle: other.draftMessageStyle,
106126
draftTimestampStyle: other.draftTimestampStyle,
127+
draftTimestampFormatter: other.draftTimestampFormatter,
107128
);
108129
}
109130

@@ -131,6 +152,8 @@ class StreamDraftListTileThemeData with Diagnosticable {
131152
b?.draftTimestampStyle,
132153
t,
133154
),
155+
draftTimestampFormatter:
156+
t < 0.5 ? a?.draftTimestampFormatter : b?.draftTimestampFormatter,
134157
);
135158

136159
@override
@@ -141,13 +164,15 @@ class StreamDraftListTileThemeData with Diagnosticable {
141164
other.backgroundColor == backgroundColor &&
142165
other.draftChannelNameStyle == draftChannelNameStyle &&
143166
other.draftMessageStyle == draftMessageStyle &&
144-
other.draftTimestampStyle == draftTimestampStyle;
167+
other.draftTimestampStyle == draftTimestampStyle &&
168+
other.draftTimestampFormatter == draftTimestampFormatter;
145169

146170
@override
147171
int get hashCode =>
148172
padding.hashCode ^
149173
backgroundColor.hashCode ^
150174
draftChannelNameStyle.hashCode ^
151175
draftMessageStyle.hashCode ^
152-
draftTimestampStyle.hashCode;
176+
draftTimestampStyle.hashCode ^
177+
draftTimestampFormatter.hashCode;
153178
}

0 commit comments

Comments
 (0)