diff --git a/e2e_app/integration_test/channel_list_test.dart b/e2e_app/integration_test/channel_list_test.dart index 4a89d3e2dd..d89a393a63 100644 --- a/e2e_app/integration_test/channel_list_test.dart +++ b/e2e_app/integration_test/channel_list_test.dart @@ -290,11 +290,7 @@ void main() { await env.userRobot.tapBackButton(); step('THEN the channel preview shows the system message'); - // The mock server attributes the truncation to whoever asked for it, so - // the system message is the current user's and the preview carries the - // "You: " prefix — which the visual formatter applies even to a system - // message, unlike the a11y label. - await env.userRobot.assertMessageInChannelPreview(systemMessage, fromCurrentUser: true); + await env.userRobot.assertMessageInChannelPreview(systemMessage); step('AND the message timestamp is shown'); await env.userRobot.assertMessagePreviewTimestamp(); diff --git a/e2e_app/integration_test/unread_counts_test.dart b/e2e_app/integration_test/unread_counts_test.dart index fe0b17c638..66f9bd94e5 100644 --- a/e2e_app/integration_test/unread_counts_test.dart +++ b/e2e_app/integration_test/unread_counts_test.dart @@ -57,7 +57,6 @@ void main() { streamTestWithEnv( allureId: '12832', description: 'new system message increments the unread count', - skip: 'https://linear.app/stream/issue/FLU-1096', body: (env) async { const systemMessage = 'System message'; diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index d529efd077..e1c5ebd5cf 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -15,6 +15,7 @@ 🐞 Fixed +- Fixed channel list previews showing a "You:" or sender name prefix, and the delivery status, in front of a system message. - Fixed a deleted message hiding its replies count, which left its thread unreachable from the channel. - Fixed `StreamMessageComposer` throwing when its channel is disposed while it still holds unsent text, for example when the user is disconnected with a channel page open. - Fixed a message that failed to send, update or delete hiding the timestamp and delivery status of the message before it. diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_item.dart b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_item.dart index c53a9b89e6..de90ce2e2d 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_item.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_item.dart @@ -695,10 +695,11 @@ class _ChannelLastMessageWithStatusState extends State<_ChannelLastMessageWithSt } final isOwnMessage = currentUser != null && latestLastMessage.user?.id == currentUser.id; + final showsDeliveryStatus = isOwnMessage && !latestLastMessage.isDeleted && !latestLastMessage.isSystem; // Show delivery status prefix only for own messages. Widget? deliveryPrefix; - if (isOwnMessage) { + if (showsDeliveryStatus) { if (widget.sendingIndicatorBuilder case final builder?) { deliveryPrefix = builder(context, latestLastMessage); } else { @@ -714,7 +715,7 @@ class _ChannelLastMessageWithStatusState extends State<_ChannelLastMessageWithSt return Row( spacing: spacing.xxs, children: [ - if (!latestLastMessage.isDeleted) ?deliveryPrefix, + ?deliveryPrefix, Flexible( child: StreamMessagePreviewText( message: latestLastMessage, diff --git a/packages/stream_chat_flutter/lib/src/utils/message_preview_formatter.dart b/packages/stream_chat_flutter/lib/src/utils/message_preview_formatter.dart index 04f97f7982..2ac61ced04 100644 --- a/packages/stream_chat_flutter/lib/src/utils/message_preview_formatter.dart +++ b/packages/stream_chat_flutter/lib/src/utils/message_preview_formatter.dart @@ -71,7 +71,7 @@ abstract interface class MessagePreviewFormatter { /// The output adapts to the message kind (regular, deleted, system, poll, /// location) and to the surrounding [channel] — for example, a "You:" /// prefix on the current user's messages, or "FirstName:" in group - /// channels. + /// channels. System messages show no prefix. /// /// [showCaption] controls how attachment and location previews label /// themselves: when `true` (the default) they display the message text; when @@ -221,7 +221,7 @@ class StreamMessagePreviewFormatter implements AccessibleMessagePreviewFormatter showCaption: showCaption, ); - if (channel == null) return content; + if (channel == null || message.isSystem) return content; if (message.user?.id == currentUser?.id) { return formatCurrentUserMessage(context, content); diff --git a/packages/stream_chat_flutter/test/src/channel/stream_message_preview_text_test.dart b/packages/stream_chat_flutter/test/src/channel/stream_message_preview_text_test.dart index dec6841433..24af17d036 100644 --- a/packages/stream_chat_flutter/test/src/channel/stream_message_preview_text_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/stream_message_preview_text_test.dart @@ -814,6 +814,42 @@ void main() { ); }); + testWidgets('group channel shows a system message from the current user without a prefix', (tester) async { + final channel = ChannelModel( + id: 'test-channel', + type: 'messaging', + memberCount: 3, + ); + + final message = Message( + text: 'Test User truncated the channel', + type: MessageType.system, + user: User(id: 'test-user-id', name: 'Test User'), + ); + + await pumpMessagePreview(tester, message, channel: channel); + + expect(find.text('Test User truncated the channel'), findsOneWidget); + }); + + testWidgets('group channel shows a system message from another user without a prefix', (tester) async { + final channel = ChannelModel( + id: 'test-channel', + type: 'messaging', + memberCount: 3, + ); + + final message = Message( + text: 'Jane Doe added Maya', + type: MessageType.system, + user: User(id: 'other-user-id', name: 'Jane Doe'), + ); + + await pumpMessagePreview(tester, message, channel: channel); + + expect(find.text('Jane Doe added Maya'), findsOneWidget); + }); + testWidgets('group channel prepends the sender name in the themed sender style', (tester) async { final channel = ChannelModel( id: 'test-channel', diff --git a/packages/stream_chat_flutter/test/src/scroll_view/channel_scroll_view/stream_channel_list_item_test.dart b/packages/stream_chat_flutter/test/src/scroll_view/channel_scroll_view/stream_channel_list_item_test.dart index 37479d57e7..fb2e3c2548 100644 --- a/packages/stream_chat_flutter/test/src/scroll_view/channel_scroll_view/stream_channel_list_item_test.dart +++ b/packages/stream_chat_flutter/test/src/scroll_view/channel_scroll_view/stream_channel_list_item_test.dart @@ -208,6 +208,82 @@ void main() { ); }); + group('ChannelListTileSubtitle', () { + const currentUserId = 'me'; + + late MockClient client; + late MockChannel channel; + late MockChannelState channelState; + + setUp(() { + client = MockClient(); + final clientState = MockClientState(); + channel = MockChannel(); + channelState = MockChannelState(); + + final currentUser = OwnUser(id: currentUserId, name: 'Me'); + + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(currentUser); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(currentUser)); + when(() => channel.state).thenReturn(channelState); + when(() => channel.client).thenReturn(client); + when(() => channelState.draft).thenReturn(null); + when(() => channelState.channelState).thenReturn(const ChannelState()); + }); + + // Pumps the channel list subtitle with [message] as the last message, + // drawing its delivery status as "sent". + Future pumpSubtitle(WidgetTester tester, Message message) async { + when(() => channelState.messages).thenReturn([message]); + when(() => channelState.messagesStream).thenAnswer((_) => Stream.value([message])); + when(() => channelState.draftStream).thenAnswer((_) => const Stream.empty()); + when(() => channelState.typingEventsStream).thenAnswer((_) => const Stream.empty()); + when(() => channelState.typingEvents).thenReturn({}); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: Scaffold( + body: ChannelListTileSubtitle( + channel: channel, + sendingIndicatorBuilder: (_, _) => const Text('sent'), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + } + + testWidgets('shows the delivery status of a message from the current user', (tester) async { + final message = Message( + text: 'hello world', + user: User(id: currentUserId), + createdAt: DateTime(2024, 1, 1), + ); + + await pumpSubtitle(tester, message); + + expect(find.text('sent'), findsOneWidget); + }); + + testWidgets('hides the delivery status of a system message from the current user', (tester) async { + final message = Message( + text: 'Me truncated the channel', + type: MessageType.system, + user: User(id: currentUserId), + createdAt: DateTime(2024, 1, 1), + ); + + await pumpSubtitle(tester, message); + + expect(find.text('Me truncated the channel'), findsOneWidget); + expect(find.text('sent'), findsNothing); + }); + }); + group('ChannelLastMessageDate', () { const currentUserId = 'me';