Skip to content

Commit a9cd6ae

Browse files
authored
feat(ui): Add MessagePreviewFormatter for customization (#2441)
1 parent 3899379 commit a9cd6ae

File tree

11 files changed

+809
-183
lines changed

11 files changed

+809
-183
lines changed

packages/stream_chat_flutter/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
✅ Added
44

5+
- Added `MessagePreviewFormatter` interface and `StreamMessagePreviewFormatter` implementation for
6+
customizing message preview text formatting in channel lists and draft messages.
7+
- Added `messagePreviewFormatter` property to `StreamChatConfigurationData` for global customization
8+
of message preview formatting.
59
- Added formatter properties to theme data classes for customizing date/timestamp
610
formatting. [[#2312]](https://github.com/GetStream/stream-chat-flutter/issues/2312) [[#2406]](https://github.com/GetStream/stream-chat-flutter/issues/2406)
711

packages/stream_chat_flutter/lib/src/channel/stream_draft_message_preview_text.dart

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,13 @@ class StreamDraftMessagePreviewText extends StatelessWidget {
1818

1919
@override
2020
Widget build(BuildContext context) {
21-
final theme = StreamChatTheme.of(context);
22-
final colorTheme = theme.colorTheme;
21+
final config = StreamChatConfiguration.of(context);
22+
final formatter = config.messagePreviewFormatter;
2323

24-
final previewTextSpan = TextSpan(
25-
text: '${context.translations.draftLabel}: ',
26-
style: textStyle?.copyWith(
27-
fontWeight: FontWeight.bold,
28-
color: colorTheme.accentPrimary,
29-
),
30-
children: [
31-
TextSpan(text: draftMessage.text, style: textStyle),
32-
],
24+
final previewTextSpan = formatter.formatDraftMessage(
25+
context,
26+
draftMessage,
27+
textStyle: textStyle,
3328
);
3429

3530
return Text.rich(

packages/stream_chat_flutter/lib/src/channel/stream_message_preview_text.dart

Lines changed: 9 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -31,32 +31,15 @@ class StreamMessagePreviewText extends StatelessWidget {
3131
final translatedMessage = message.translate(translationLanguage);
3232
final previewMessage = translatedMessage.replaceMentions(linkify: false);
3333

34-
final previewText = _getPreviewText(context, previewMessage, currentUser);
35-
36-
final mentionedUsers = message.mentionedUsers;
37-
final mentionedUsersRegex = RegExp(
38-
mentionedUsers.map((it) => '@${it.name}').join('|'),
39-
);
40-
41-
final previewTextSpan = TextSpan(
42-
children: [
43-
...previewText.splitByRegExp(mentionedUsersRegex).map(
44-
(text) {
45-
// Bold the text if it is a mention user.
46-
if (mentionedUsers.any((it) => '@${it.name}' == text)) {
47-
return TextSpan(
48-
text: text,
49-
style: textStyle?.copyWith(fontWeight: FontWeight.bold),
50-
);
51-
}
52-
53-
return TextSpan(
54-
text: text,
55-
style: textStyle,
56-
);
57-
},
58-
)
59-
],
34+
final config = StreamChatConfiguration.of(context);
35+
final formatter = config.messagePreviewFormatter;
36+
37+
final previewTextSpan = formatter.formatMessage(
38+
context,
39+
previewMessage,
40+
channel: channel,
41+
currentUser: currentUser,
42+
textStyle: textStyle,
6043
);
6144

6245
return Text.rich(
@@ -66,144 +49,4 @@ class StreamMessagePreviewText extends StatelessWidget {
6649
textAlign: TextAlign.start,
6750
);
6851
}
69-
70-
String _getPreviewText(
71-
BuildContext context,
72-
Message message,
73-
User currentUser,
74-
) {
75-
final translations = context.translations;
76-
77-
if (message.isDeleted) {
78-
return translations.messageDeletedLabel;
79-
}
80-
81-
if (message.isSystem) {
82-
return message.text ?? translations.systemMessageLabel;
83-
}
84-
85-
if (message.poll case final poll?) {
86-
return _pollPreviewText(context, poll, currentUser);
87-
}
88-
89-
final previewText = _previewMessageContextText(context, message);
90-
if (previewText == null) return translations.emptyMessagePreviewText;
91-
92-
if (channel case final channel?) {
93-
if (message.user?.id == currentUser.id) {
94-
return '${translations.youText}: $previewText';
95-
}
96-
97-
if (channel.memberCount > 2) {
98-
return '${message.user?.name}: $previewText';
99-
}
100-
}
101-
102-
return previewText;
103-
}
104-
105-
String _pollPreviewText(
106-
BuildContext context,
107-
Poll poll,
108-
User currentUser,
109-
) {
110-
final translations = context.translations;
111-
112-
// If the poll already contains some votes, we will preview the latest voter
113-
// and the poll name
114-
if (poll.latestVotes.firstOrNull?.user case final latestVoter?) {
115-
if (latestVoter.id == currentUser.id) {
116-
final youVoted = translations.pollYouVotedText;
117-
return '📊 $youVoted: "${poll.name}"';
118-
}
119-
120-
final someoneVoted = translations.pollSomeoneVotedText(latestVoter.name);
121-
return '📊 $someoneVoted: "${poll.name}"';
122-
}
123-
124-
// Otherwise, we will show the creator of the poll and the poll name
125-
if (poll.createdBy case final creator?) {
126-
if (creator.id == currentUser.id) {
127-
final youCreated = translations.pollYouCreatedText;
128-
return '📊 $youCreated: "${poll.name}"';
129-
}
130-
131-
final someoneCreated = translations.pollSomeoneCreatedText(creator.name);
132-
return '📊 $someoneCreated: "${poll.name}"';
133-
}
134-
135-
// Otherwise, we will show the poll name if it exists.
136-
if (poll.name.trim() case final pollName when pollName.isNotEmpty) {
137-
return '📊 $pollName';
138-
}
139-
140-
// If nothing else, we will show the default poll emoji.
141-
return '📊';
142-
}
143-
144-
String? _previewMessageContextText(
145-
BuildContext context,
146-
Message message,
147-
) {
148-
final translations = context.translations;
149-
150-
final messageText = switch (message.text) {
151-
final messageText? when messageText.isNotEmpty => messageText,
152-
_ => null,
153-
};
154-
155-
// If the message contains some attachments, we will show the first one
156-
// and the text if it exists.
157-
if (message.attachments.firstOrNull case final attachment?) {
158-
final attachmentIcon = switch (attachment.type) {
159-
AttachmentType.audio => '🎧',
160-
AttachmentType.file => '📄',
161-
AttachmentType.image => '📷',
162-
AttachmentType.video => '📹',
163-
AttachmentType.giphy => '/giphy',
164-
AttachmentType.voiceRecording => '🎤',
165-
_ => null,
166-
};
167-
168-
final attachmentTitle = switch (attachment.type) {
169-
AttachmentType.audio => messageText ?? translations.audioAttachmentText,
170-
AttachmentType.file => attachment.title ?? messageText,
171-
AttachmentType.image => messageText ?? translations.imageAttachmentText,
172-
AttachmentType.video => messageText ?? translations.videoAttachmentText,
173-
AttachmentType.giphy => messageText,
174-
AttachmentType.voiceRecording => translations.voiceRecordingText,
175-
_ => null,
176-
};
177-
178-
if (attachmentIcon != null || attachmentTitle != null) {
179-
return [attachmentIcon, attachmentTitle].nonNulls.join(' ');
180-
}
181-
}
182-
183-
return messageText;
184-
}
185-
}
186-
187-
extension on String {
188-
List<String> splitByRegExp(RegExp regex) {
189-
// If the pattern is empty, return the whole string
190-
if (regex.pattern.isEmpty) return [this];
191-
192-
final result = <String>[];
193-
var start = 0;
194-
195-
for (final match in regex.allMatches(this)) {
196-
if (match.start > start) {
197-
result.add(substring(start, match.start));
198-
}
199-
result.add(match.group(0)!);
200-
start = match.end;
201-
}
202-
203-
if (start < length) {
204-
result.add(substring(start));
205-
}
206-
207-
return result;
208-
}
20952
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'package:collection/collection.dart';
22
import 'package:flutter/material.dart';
33
import 'package:rxdart/rxdart.dart';
4-
import 'package:stream_chat_flutter/src/channel/stream_draft_message_preview_text.dart';
54
import 'package:stream_chat_flutter/src/message_widget/sending_indicator_builder.dart';
65
import 'package:stream_chat_flutter/src/misc/empty_widget.dart';
76
import 'package:stream_chat_flutter/src/misc/timestamp.dart';

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import 'package:flutter/material.dart';
2-
import 'package:stream_chat_flutter/src/channel/stream_draft_message_preview_text.dart';
32
import 'package:stream_chat_flutter/src/misc/timestamp.dart';
43
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
54

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import 'package:collection/collection.dart';
22
import 'package:flutter/material.dart';
3-
import 'package:stream_chat_flutter/src/channel/stream_draft_message_preview_text.dart';
43
import 'package:stream_chat_flutter/src/misc/timestamp.dart';
54
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
65

packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class StreamChatConfigurationData {
115115
List<StreamReactionIcon>? reactionIcons,
116116
bool? enforceUniqueReactions,
117117
bool draftMessagesEnabled = false,
118+
MessagePreviewFormatter? messagePreviewFormatter,
118119
}) {
119120
return StreamChatConfigurationData._(
120121
loadingIndicator: loadingIndicator,
@@ -123,6 +124,8 @@ class StreamChatConfigurationData {
123124
reactionIcons: reactionIcons ?? _defaultReactionIcons,
124125
enforceUniqueReactions: enforceUniqueReactions ?? true,
125126
draftMessagesEnabled: draftMessagesEnabled,
127+
messagePreviewFormatter:
128+
messagePreviewFormatter ?? MessagePreviewFormatter(),
126129
);
127130
}
128131

@@ -133,6 +136,7 @@ class StreamChatConfigurationData {
133136
required this.reactionIcons,
134137
required this.enforceUniqueReactions,
135138
required this.draftMessagesEnabled,
139+
required this.messagePreviewFormatter,
136140
});
137141

138142
/// Copies the configuration options from one [StreamChatConfigurationData] to
@@ -144,6 +148,7 @@ class StreamChatConfigurationData {
144148
List<StreamReactionIcon>? reactionIcons,
145149
bool? enforceUniqueReactions,
146150
bool? draftMessagesEnabled,
151+
MessagePreviewFormatter? messagePreviewFormatter,
147152
}) {
148153
return StreamChatConfigurationData(
149154
reactionIcons: reactionIcons ?? this.reactionIcons,
@@ -153,6 +158,8 @@ class StreamChatConfigurationData {
153158
enforceUniqueReactions:
154159
enforceUniqueReactions ?? this.enforceUniqueReactions,
155160
draftMessagesEnabled: draftMessagesEnabled ?? this.draftMessagesEnabled,
161+
messagePreviewFormatter:
162+
messagePreviewFormatter ?? this.messagePreviewFormatter,
156163
);
157164
}
158165

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

186+
/// The formatter used for message previews throughout the application.
187+
///
188+
/// Defaults to [MessagePreviewFormatter].
189+
final MessagePreviewFormatter messagePreviewFormatter;
190+
179191
static final _defaultReactionIcons = [
180192
StreamReactionIcon(
181193
type: 'love',

0 commit comments

Comments
 (0)