Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

✅ Added

- Added `MessagePreviewFormatter` interface and `StreamMessagePreviewFormatter` implementation for
customizing message preview text formatting in channel lists and draft messages.
- Added `messagePreviewFormatter` property to `StreamChatConfigurationData` for global customization
of message preview formatting.
- 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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,13 @@ class StreamDraftMessagePreviewText extends StatelessWidget {

@override
Widget build(BuildContext context) {
final theme = StreamChatTheme.of(context);
final colorTheme = theme.colorTheme;
final config = StreamChatConfiguration.of(context);
final formatter = config.messagePreviewFormatter;

final previewTextSpan = TextSpan(
text: '${context.translations.draftLabel}: ',
style: textStyle?.copyWith(
fontWeight: FontWeight.bold,
color: colorTheme.accentPrimary,
),
children: [
TextSpan(text: draftMessage.text, style: textStyle),
],
final previewTextSpan = formatter.formatDraftMessage(
context,
draftMessage,
textStyle: textStyle,
);

return Text.rich(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,32 +31,15 @@ class StreamMessagePreviewText extends StatelessWidget {
final translatedMessage = message.translate(translationLanguage);
final previewMessage = translatedMessage.replaceMentions(linkify: false);

final previewText = _getPreviewText(context, previewMessage, currentUser);

final mentionedUsers = message.mentionedUsers;
final mentionedUsersRegex = RegExp(
mentionedUsers.map((it) => '@${it.name}').join('|'),
);

final previewTextSpan = TextSpan(
children: [
...previewText.splitByRegExp(mentionedUsersRegex).map(
(text) {
// Bold the text if it is a mention user.
if (mentionedUsers.any((it) => '@${it.name}' == text)) {
return TextSpan(
text: text,
style: textStyle?.copyWith(fontWeight: FontWeight.bold),
);
}

return TextSpan(
text: text,
style: textStyle,
);
},
)
],
final config = StreamChatConfiguration.of(context);
final formatter = config.messagePreviewFormatter;

final previewTextSpan = formatter.formatMessage(
context,
previewMessage,
channel: channel,
currentUser: currentUser,
textStyle: textStyle,
);

return Text.rich(
Expand All @@ -66,144 +49,4 @@ class StreamMessagePreviewText extends StatelessWidget {
textAlign: TextAlign.start,
);
}

String _getPreviewText(
BuildContext context,
Message message,
User currentUser,
) {
final translations = context.translations;

if (message.isDeleted) {
return translations.messageDeletedLabel;
}

if (message.isSystem) {
return message.text ?? translations.systemMessageLabel;
}

if (message.poll case final poll?) {
return _pollPreviewText(context, poll, currentUser);
}

final previewText = _previewMessageContextText(context, message);
if (previewText == null) return translations.emptyMessagePreviewText;

if (channel case final channel?) {
if (message.user?.id == currentUser.id) {
return '${translations.youText}: $previewText';
}

if (channel.memberCount > 2) {
return '${message.user?.name}: $previewText';
}
}

return previewText;
}

String _pollPreviewText(
BuildContext context,
Poll poll,
User currentUser,
) {
final translations = context.translations;

// If the poll already contains some votes, we will preview the latest voter
// and the poll name
if (poll.latestVotes.firstOrNull?.user case final latestVoter?) {
if (latestVoter.id == currentUser.id) {
final youVoted = translations.pollYouVotedText;
return '📊 $youVoted: "${poll.name}"';
}

final someoneVoted = translations.pollSomeoneVotedText(latestVoter.name);
return '📊 $someoneVoted: "${poll.name}"';
}

// Otherwise, we will show the creator of the poll and the poll name
if (poll.createdBy case final creator?) {
if (creator.id == currentUser.id) {
final youCreated = translations.pollYouCreatedText;
return '📊 $youCreated: "${poll.name}"';
}

final someoneCreated = translations.pollSomeoneCreatedText(creator.name);
return '📊 $someoneCreated: "${poll.name}"';
}

// Otherwise, we will show the poll name if it exists.
if (poll.name.trim() case final pollName when pollName.isNotEmpty) {
return '📊 $pollName';
}

// If nothing else, we will show the default poll emoji.
return '📊';
}

String? _previewMessageContextText(
BuildContext context,
Message message,
) {
final translations = context.translations;

final messageText = switch (message.text) {
final messageText? when messageText.isNotEmpty => messageText,
_ => null,
};

// If the message contains some attachments, we will show the first one
// and the text if it exists.
if (message.attachments.firstOrNull case final attachment?) {
final attachmentIcon = switch (attachment.type) {
AttachmentType.audio => '🎧',
AttachmentType.file => '📄',
AttachmentType.image => '📷',
AttachmentType.video => '📹',
AttachmentType.giphy => '/giphy',
AttachmentType.voiceRecording => '🎤',
_ => null,
};

final attachmentTitle = switch (attachment.type) {
AttachmentType.audio => messageText ?? translations.audioAttachmentText,
AttachmentType.file => attachment.title ?? messageText,
AttachmentType.image => messageText ?? translations.imageAttachmentText,
AttachmentType.video => messageText ?? translations.videoAttachmentText,
AttachmentType.giphy => messageText,
AttachmentType.voiceRecording => translations.voiceRecordingText,
_ => null,
};

if (attachmentIcon != null || attachmentTitle != null) {
return [attachmentIcon, attachmentTitle].nonNulls.join(' ');
}
}

return messageText;
}
}

extension on String {
List<String> splitByRegExp(RegExp regex) {
// If the pattern is empty, return the whole string
if (regex.pattern.isEmpty) return [this];

final result = <String>[];
var start = 0;

for (final match in regex.allMatches(this)) {
if (match.start > start) {
result.add(substring(start, match.start));
}
result.add(match.group(0)!);
start = match.end;
}

if (start < length) {
result.add(substring(start));
}

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class StreamChatConfigurationData {
List<StreamReactionIcon>? reactionIcons,
bool? enforceUniqueReactions,
bool draftMessagesEnabled = false,
MessagePreviewFormatter? messagePreviewFormatter,
}) {
return StreamChatConfigurationData._(
loadingIndicator: loadingIndicator,
Expand All @@ -123,6 +124,8 @@ class StreamChatConfigurationData {
reactionIcons: reactionIcons ?? _defaultReactionIcons,
enforceUniqueReactions: enforceUniqueReactions ?? true,
draftMessagesEnabled: draftMessagesEnabled,
messagePreviewFormatter:
messagePreviewFormatter ?? MessagePreviewFormatter(),
);
}

Expand All @@ -133,6 +136,7 @@ class StreamChatConfigurationData {
required this.reactionIcons,
required this.enforceUniqueReactions,
required this.draftMessagesEnabled,
required this.messagePreviewFormatter,
});

/// Copies the configuration options from one [StreamChatConfigurationData] to
Expand All @@ -144,6 +148,7 @@ class StreamChatConfigurationData {
List<StreamReactionIcon>? reactionIcons,
bool? enforceUniqueReactions,
bool? draftMessagesEnabled,
MessagePreviewFormatter? messagePreviewFormatter,
}) {
return StreamChatConfigurationData(
reactionIcons: reactionIcons ?? this.reactionIcons,
Expand All @@ -153,6 +158,8 @@ class StreamChatConfigurationData {
enforceUniqueReactions:
enforceUniqueReactions ?? this.enforceUniqueReactions,
draftMessagesEnabled: draftMessagesEnabled ?? this.draftMessagesEnabled,
messagePreviewFormatter:
messagePreviewFormatter ?? this.messagePreviewFormatter,
);
}

Expand All @@ -176,6 +183,11 @@ class StreamChatConfigurationData {
/// Whether a new reaction should replace the existing one.
final bool enforceUniqueReactions;

/// The formatter used for message previews throughout the application.
///
/// Defaults to [MessagePreviewFormatter].
final MessagePreviewFormatter messagePreviewFormatter;

static final _defaultReactionIcons = [
StreamReactionIcon(
type: 'love',
Expand Down
Loading
Loading