Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
🐛 Fixed

- Fixed a use-after-dispose race condition in `StreamAttachmentPickerController`, `StreamAudioRecorderController`, and `StreamAudioPlaylistController`: async methods could write `value` after `dispose()`, causing a `notifyListeners()` assertion throw in debug mode. All three now use the `DisposeAwareValueNotifier` mixin from `stream_chat_flutter_core`.
- Fixed shadowed messages not hidden in channel list items.

✅ Added

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,10 @@ class _ChannelLastMessageWithStatus extends StatefulWidget {
class _ChannelLastMessageWithStatusState extends State<_ChannelLastMessageWithStatus> {
Message? _currentLastMessage;

static bool _defaultLastMessagePredicate(Message message) {
bool _defaultLastMessagePredicate(Message message) {
final currentUserId = widget.channel.client.state.currentUser?.id;
final isMyMessage = currentUserId != null && message.user?.id == currentUserId;
if (message.shadowed && !isMyMessage) return false;
if (message.isShadowed) return false;
if (message.isError) return false;
if (message.isEphemeral) return false;
Expand Down Expand Up @@ -675,11 +678,13 @@ class ChannelLastMessageText extends StatefulWidget {
super.key,
required this.channel,
this.textStyle,
this.lastMessagePredicate = _defaultLastMessagePredicate,
bool Function(Message)? lastMessagePredicate,
}) : assert(
channel.state != null,
'Channel ${channel.id} is not initialized',
);
),
lastMessagePredicate =
lastMessagePredicate ?? _defaultLastMessagePredicate(channel.client.state.currentUser?.id);

/// The channel to display the last message of.
final Channel channel;
Expand All @@ -696,12 +701,15 @@ class ChannelLastMessageText extends StatefulWidget {

// The default predicate to determine if the message should be
// considered for the last message.
static bool _defaultLastMessagePredicate(Message message) {
if (message.isShadowed) return false;
if (message.isError) return false;
if (message.isEphemeral) return false;

return true;
static bool Function(Message) _defaultLastMessagePredicate(String? currentUserId) {
return (Message message) {
final isMyMessage = currentUserId != null && message.user?.id == currentUserId;
if (message.shadowed && !isMyMessage) return false;
if (message.isShadowed) return false;
if (message.isError) return false;
if (message.isEphemeral) return false;
return true;
};
}

@override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

import '../../mocks.dart';
Comment thread
coderabbitai[bot] marked this conversation as resolved.

void main() {
const currentUserId = 'me';

late MockClient client;
late MockClientState clientState;
late MockChannel channel;
late MockChannelState channelState;

setUp(() {
client = MockClient();
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());
});

Future<void> pumpWithMessages(
WidgetTester tester,
List<Message> messages,
) async {
when(() => channelState.messages).thenReturn(messages);
when(() => channelState.messagesStream).thenAnswer((_) => Stream.value(messages));

await tester.pumpWidget(
MaterialApp(
home: StreamChat(
client: client,
child: Scaffold(
body: ChannelLastMessageText(channel: channel),
),
),
),
);
await tester.pumpAndSettle();
}

testWidgets('shows the latest message text as preview', (tester) async {
final message = Message(
text: 'hello world',
user: User(id: 'other'),
createdAt: DateTime(2024, 1, 1),
);

await pumpWithMessages(tester, [message]);

expect(find.text('hello world'), findsOneWidget);
});

testWidgets('hides shadowed message from another user', (tester) async {
final visible = Message(
text: 'visible',
user: User(id: 'other'),
createdAt: DateTime(2024, 1, 1),
);
final shadowed = Message(
text: 'shadowed',
shadowed: true,
user: User(id: 'other'),
createdAt: DateTime(2024, 1, 2),
);

await pumpWithMessages(tester, [visible, shadowed]);

expect(find.text('visible'), findsOneWidget);
expect(find.text('shadowed'), findsNothing);
});

testWidgets(
'shows current user own shadowed message',
(tester) async {
final mine = Message(
text: 'my shadowed',
shadowed: true,
user: User(id: currentUserId),
createdAt: DateTime(2024, 1, 1),
);

await pumpWithMessages(tester, [mine]);

expect(find.text('my shadowed'), findsOneWidget);
},
);

testWidgets(
'hides moderation V2 shadowed message regardless of sender',
(tester) async {
final visible = Message(
text: 'visible',
user: User(id: currentUserId),
createdAt: DateTime(2024, 1, 1),
);
final v2Shadowed = Message(
text: 'v2 shadowed',
user: User(id: currentUserId),
createdAt: DateTime(2024, 1, 2),
moderation: const Moderation(
action: ModerationAction.shadow,
originalText: 'v2 shadowed',
),
);

await pumpWithMessages(tester, [visible, v2Shadowed]);

expect(find.text('visible'), findsOneWidget);
expect(find.text('v2 shadowed'), findsNothing);
},
);

testWidgets('hides error and ephemeral messages', (tester) async {
final visible = Message(
text: 'visible',
user: User(id: 'other'),
createdAt: DateTime(2024, 1, 1),
);
final errored = Message(
text: 'errored',
type: MessageType.error,
user: User(id: 'other'),
createdAt: DateTime(2024, 1, 2),
);
final ephemeral = Message(
text: 'ephemeral',
type: MessageType.ephemeral,
user: User(id: 'other'),
createdAt: DateTime(2024, 1, 3),
);

await pumpWithMessages(tester, [visible, errored, ephemeral]);

expect(find.text('visible'), findsOneWidget);
expect(find.text('errored'), findsNothing);
expect(find.text('ephemeral'), findsNothing);
});

testWidgets(
'custom lastMessagePredicate fully replaces the default',
(tester) async {
final shadowedByOther = Message(
text: 'shadowed by other',
shadowed: true,
user: User(id: 'other'),
createdAt: DateTime(2024, 1, 1),
);

when(() => channelState.messages).thenReturn([shadowedByOther]);
when(() => channelState.messagesStream).thenAnswer((_) => Stream.value([shadowedByOther]));

await tester.pumpWidget(
MaterialApp(
home: StreamChat(
client: client,
child: Scaffold(
body: ChannelLastMessageText(
channel: channel,
lastMessagePredicate: (_) => true,
),
),
),
),
);
await tester.pumpAndSettle();

expect(find.text('shadowed by other'), findsOneWidget);
},
);
}
Loading