Skip to content
Open
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
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,20 +584,13 @@ class _ChannelLastMessageWithStatus extends StatefulWidget {
class _ChannelLastMessageWithStatusState extends State<_ChannelLastMessageWithStatus> {
Message? _currentLastMessage;

static bool _defaultLastMessagePredicate(Message message) {
if (message.isShadowed) return false;
if (message.isError) return false;
if (message.isEphemeral) return false;

return true;
}

@override
Widget build(BuildContext context) {
final channelState = widget.channel.state;
if (channelState == null) return const Empty();

final currentUser = widget.channel.client.state.currentUser;
final predicate = _defaultLastMessagePredicate(currentUser?.id);

return BetterStreamBuilder<(Draft?, List<Message>)>(
stream: CombineLatestStream.combine2(
Expand All @@ -623,7 +616,7 @@ class _ChannelLastMessageWithStatusState extends State<_ChannelLastMessageWithSt
}

// Find the last valid message.
final message = messages.lastWhereOrNull(_defaultLastMessagePredicate);
final message = messages.lastWhereOrNull(predicate);
final latestLastMessage = [message, _currentLastMessage].latest;

if (latestLastMessage == null) {
Expand Down Expand Up @@ -675,11 +668,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 @@ -694,18 +689,20 @@ class ChannelLastMessageText extends StatefulWidget {
/// considered for the last message.
final bool Function(Message) lastMessagePredicate;

// 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;
@override
State<ChannelLastMessageText> createState() => _ChannelLastMessageTextState();
}

// The default predicate to determine if the message should be
// considered for the last message.
bool Function(Message) _defaultLastMessagePredicate(String? currentUserId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit confusing, I think it's already clearer with a typedef. Something like this:

typedef LastMessagePredicate = bool Function(Message);
LastMessagePredicate _defaultLastMessagePredicateForUser(String? currentUserId) {

return (Message message) {
final isMyMessage = currentUserId != null && message.user?.id == currentUserId;
if (message.shadowed && !isMyMessage) return false;
if (message.isError) return false;
if (message.isEphemeral) return false;

return true;
}

@override
State<ChannelLastMessageText> createState() => _ChannelLastMessageTextState();
};
}

class _ChannelLastMessageTextState extends State<ChannelLastMessageText> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
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 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