Skip to content
Merged
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
5 changes: 5 additions & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## Upcoming

✅ Added

- Added formatter properties to theme data classes for customizing date/timestamp
formatting. [[#2312]](https://github.com/GetStream/stream-chat-flutter/issues/2312) [[#2406]](https://github.com/GetStream/stream-chat-flutter/issues/2406)

🐞 Fixed

- Fixed mistakenly passing the hyperlink text to the `onLinkTap` callback instead of the actual `href`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,13 @@ class BottomRow extends StatelessWidget {
StreamTimestamp(
date: message.createdAt.toLocal(),
style: messageTheme.createdAtStyle,
formatter: (_, date) => Jiffy.parseFromDateTime(date).jm,
formatter: (context, date) {
if (messageTheme.createdAtFormatter case final formatter?) {
return formatter.call(context, date);
}

return Jiffy.parseFromDateTime(date).jm;
},
),
];

Expand Down
10 changes: 10 additions & 0 deletions packages/stream_chat_flutter/lib/src/misc/date_divider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class StreamDateDivider extends StatelessWidget {
super.key,
required this.dateTime,
this.uppercase = false,
this.formatter,
});

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

/// Custom formatter for the date
final DateFormatter? formatter;

@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
Expand All @@ -36,6 +40,12 @@ class StreamDateDivider extends StatelessWidget {
color: chatThemeData.colorTheme.barsBg,
),
formatter: (context, date) {
if (formatter case final formatter?) {
final timestamp = formatter.call(context, date);
if (uppercase) return timestamp.toUpperCase();
return timestamp;
}

final timestamp = switch (date) {
_ when date.isToday => context.translations.todayLabel,
_ when date.isYesterday => context.translations.yesterdayLabel,
Expand Down
4 changes: 2 additions & 2 deletions packages/stream_chat_flutter/lib/src/misc/timestamp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ class StreamTimestamp extends StatelessWidget {
const StreamTimestamp({
super.key,
required this.date,
this.formatter = formatDate,
DateFormatter? formatter,
this.style,
this.textAlign,
this.textDirection,
});
}) : formatter = formatter ?? formatDate;

/// The date to show in the timestamp.
final DateTime date;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:stream_chat_flutter/src/channel/stream_draft_message_preview_tex
import 'package:stream_chat_flutter/src/message_widget/sending_indicator_builder.dart';
import 'package:stream_chat_flutter/src/misc/empty_widget.dart';
import 'package:stream_chat_flutter/src/misc/timestamp.dart';
import 'package:stream_chat_flutter/src/utils/date_formatter.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

/// A widget that displays a channel preview.
Expand Down Expand Up @@ -174,6 +175,7 @@ class StreamChannelListTile extends StatelessWidget {
ChannelLastMessageDate(
channel: channel,
textStyle: channelPreviewTheme.lastMessageAtStyle,
formatter: channelPreviewTheme.lastMessageAtFormatter,
);

return BetterStreamBuilder<bool>(
Expand Down Expand Up @@ -265,6 +267,7 @@ class ChannelLastMessageDate extends StatelessWidget {
super.key,
required this.channel,
this.textStyle,
this.formatter,
}) : assert(
channel.state != null,
'Channel ${channel.id} is not initialized',
Expand All @@ -276,17 +279,21 @@ class ChannelLastMessageDate extends StatelessWidget {
/// The style of the text displayed
final TextStyle? textStyle;

/// The formatter to format the date.
final DateFormatter? formatter;

@override
Widget build(BuildContext context) => BetterStreamBuilder<DateTime>(
stream: channel.lastMessageAtStream,
initialData: channel.lastMessageAt,
builder: (context, lastMessageAt) {
return StreamTimestamp(
date: lastMessageAt.toLocal(),
style: textStyle,
);
},
);
Widget build(BuildContext context) {
return BetterStreamBuilder<DateTime>(
stream: channel.lastMessageAtStream,
initialData: channel.lastMessageAt,
builder: (context, lastMessageAt) => StreamTimestamp(
date: lastMessageAt.toLocal(),
style: textStyle,
formatter: formatter,
),
);
}
}

/// A widget that displays the subtitle for [StreamChannelListTile].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class DraftMessageContent extends StatelessWidget {
StreamTimestamp(
date: draft.createdAt.toLocal(),
style: theme.draftTimestampStyle,
formatter: theme.draftTimestampFormatter,
),
],
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/misc/timestamp.dart';
import 'package:stream_chat_flutter/src/utils/date_formatter.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

/// A widget that displays a message search item.
Expand Down Expand Up @@ -140,6 +141,7 @@ class StreamMessageSearchListTile extends StatelessWidget {
MessageSearchTileMessageDate(
message: message,
textStyle: channelPreviewTheme.lastMessageAtStyle,
formatter: channelPreviewTheme.lastMessageAtFormatter,
),
],
);
Expand Down Expand Up @@ -212,6 +214,7 @@ class MessageSearchTileMessageDate extends StatelessWidget {
super.key,
required this.message,
this.textStyle,
this.formatter,
});

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

/// An optional formatter to format the date.
final DateFormatter? formatter;

@override
Widget build(BuildContext context) {
final createdAt = message.createdAt;
return StreamTimestamp(
date: createdAt.toLocal(),
style: textStyle,
formatter: formatter,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ class ThreadLatestReply extends StatelessWidget {
StreamTimestamp(
date: latestReply.createdAt.toLocal(),
style: theme.threadLatestReplyTimestampStyle,
formatter: theme.threadLatestReplyTimestampFormatter,
),
],
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/theme/avatar_theme.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/utils/date_formatter.dart';

/// {@template channelPreviewTheme}
/// Overrides the default style of [ChannelPreview] descendants.
Expand Down Expand Up @@ -70,6 +71,7 @@ class StreamChannelPreviewThemeData with Diagnosticable {
this.avatarTheme,
this.unreadCounterColor,
this.indicatorIconSize,
this.lastMessageAtFormatter,
});

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

/// Formatter for the last message timestamp.
///
/// If null, uses the default date formatting.
///
/// Example:
/// ```dart
/// StreamChannelPreviewThemeData(
/// lastMessageAtStyle: TextStyle(...),
/// lastMessageAtFormatter: (context, date) {
/// return Jiffy.parseFromDateTime(date).format('d MMMM'); // "23 May"
/// },
/// )
/// ```
final DateFormatter? lastMessageAtFormatter;

/// Copy with theme
StreamChannelPreviewThemeData copyWith({
TextStyle? titleStyle,
Expand All @@ -98,6 +115,7 @@ class StreamChannelPreviewThemeData with Diagnosticable {
StreamAvatarThemeData? avatarTheme,
Color? unreadCounterColor,
double? indicatorIconSize,
DateFormatter? lastMessageAtFormatter,
}) {
return StreamChannelPreviewThemeData(
titleStyle: titleStyle ?? this.titleStyle,
Expand All @@ -106,6 +124,8 @@ class StreamChannelPreviewThemeData with Diagnosticable {
avatarTheme: avatarTheme ?? this.avatarTheme,
unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor,
indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize,
lastMessageAtFormatter:
lastMessageAtFormatter ?? this.lastMessageAtFormatter,
);
}

Expand All @@ -125,6 +145,8 @@ class StreamChannelPreviewThemeData with Diagnosticable {
titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t),
unreadCounterColor:
Color.lerp(a.unreadCounterColor, b.unreadCounterColor, t),
lastMessageAtFormatter:
t < 0.5 ? a.lastMessageAtFormatter : b.lastMessageAtFormatter,
);
}

Expand All @@ -139,6 +161,8 @@ class StreamChannelPreviewThemeData with Diagnosticable {
other.lastMessageAtStyle,
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
unreadCounterColor: other.unreadCounterColor,
lastMessageAtFormatter:
other.lastMessageAtFormatter ?? lastMessageAtFormatter,
);
}

Expand All @@ -152,7 +176,8 @@ class StreamChannelPreviewThemeData with Diagnosticable {
lastMessageAtStyle == other.lastMessageAtStyle &&
avatarTheme == other.avatarTheme &&
unreadCounterColor == other.unreadCounterColor &&
indicatorIconSize == other.indicatorIconSize;
indicatorIconSize == other.indicatorIconSize &&
lastMessageAtFormatter == other.lastMessageAtFormatter;

@override
int get hashCode =>
Expand All @@ -161,7 +186,8 @@ class StreamChannelPreviewThemeData with Diagnosticable {
lastMessageAtStyle.hashCode ^
avatarTheme.hashCode ^
unreadCounterColor.hashCode ^
indicatorIconSize.hashCode;
indicatorIconSize.hashCode ^
lastMessageAtFormatter.hashCode;

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
Expand All @@ -171,6 +197,8 @@ class StreamChannelPreviewThemeData with Diagnosticable {
..add(DiagnosticsProperty('subtitleStyle', subtitleStyle))
..add(DiagnosticsProperty('lastMessageAtStyle', lastMessageAtStyle))
..add(DiagnosticsProperty('avatarTheme', avatarTheme))
..add(ColorProperty('unreadCounterColor', unreadCounterColor));
..add(ColorProperty('unreadCounterColor', unreadCounterColor))
..add(DiagnosticsProperty(
'lastMessageAtFormatter', lastMessageAtFormatter));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/utils/date_formatter.dart';

/// {@template streamDraftListTileTheme}
/// Overrides the default style of [StreamDraftListTile] descendants.
Expand Down Expand Up @@ -57,6 +58,7 @@ class StreamDraftListTileThemeData with Diagnosticable {
this.draftChannelNameStyle,
this.draftMessageStyle,
this.draftTimestampStyle,
this.draftTimestampFormatter,
});

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

/// Formatter for the draft timestamp.
///
/// If null, uses the default date formatting.
///
/// Example:
/// ```dart
/// StreamDraftListTileThemeData(
/// draftTimestampStyle: TextStyle(...),
/// draftTimestampFormatter: (context, date) {
/// return Jiffy.parseFromDateTime(date).fromNow(); // "2 hours ago"
/// },
/// )
/// ```
final DateFormatter? draftTimestampFormatter;

/// A copy of [StreamDraftListTileThemeData] with specified attributes
/// overridden.
StreamDraftListTileThemeData copyWith({
Expand All @@ -82,6 +99,7 @@ class StreamDraftListTileThemeData with Diagnosticable {
TextStyle? draftChannelNameStyle,
TextStyle? draftMessageStyle,
TextStyle? draftTimestampStyle,
DateFormatter? draftTimestampFormatter,
Color? draftIconColor,
}) =>
StreamDraftListTileThemeData(
Expand All @@ -91,6 +109,8 @@ class StreamDraftListTileThemeData with Diagnosticable {
draftChannelNameStyle ?? this.draftChannelNameStyle,
draftMessageStyle: draftMessageStyle ?? this.draftMessageStyle,
draftTimestampStyle: draftTimestampStyle ?? this.draftTimestampStyle,
draftTimestampFormatter:
draftTimestampFormatter ?? this.draftTimestampFormatter,
);

/// Merges this [StreamDraftListTileThemeData] with the [other].
Expand All @@ -104,6 +124,7 @@ class StreamDraftListTileThemeData with Diagnosticable {
draftChannelNameStyle: other.draftChannelNameStyle,
draftMessageStyle: other.draftMessageStyle,
draftTimestampStyle: other.draftTimestampStyle,
draftTimestampFormatter: other.draftTimestampFormatter,
);
}

Expand Down Expand Up @@ -131,6 +152,8 @@ class StreamDraftListTileThemeData with Diagnosticable {
b?.draftTimestampStyle,
t,
),
draftTimestampFormatter:
t < 0.5 ? a?.draftTimestampFormatter : b?.draftTimestampFormatter,
);

@override
Expand All @@ -141,13 +164,15 @@ class StreamDraftListTileThemeData with Diagnosticable {
other.backgroundColor == backgroundColor &&
other.draftChannelNameStyle == draftChannelNameStyle &&
other.draftMessageStyle == draftMessageStyle &&
other.draftTimestampStyle == draftTimestampStyle;
other.draftTimestampStyle == draftTimestampStyle &&
other.draftTimestampFormatter == draftTimestampFormatter;

@override
int get hashCode =>
padding.hashCode ^
backgroundColor.hashCode ^
draftChannelNameStyle.hashCode ^
draftMessageStyle.hashCode ^
draftTimestampStyle.hashCode;
draftTimestampStyle.hashCode ^
draftTimestampFormatter.hashCode;
}
Loading