From 2e28c79145ea392a36325d829261ed154369101c Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 3 Sep 2026 14:23:34 +0200 Subject: [PATCH 1/2] fix(llc): keep Channel.memberCount fresh from channel events (#2933) `Channel.memberCount` / `memberCountStream` only ever reflected the `member_count` returned by `query` / `watch` and `channel.updated`. The `member.added` / `member.removed` handlers update the loaded member list but leave the count untouched, so it went stale for the rest of the session as soon as anyone joined or left. The backend already ships the authoritative count as `channel_member_count` event metadata on channel events, next to the `channel_message_count` we already consume. Parse it into `Event.channelMemberCount` and apply it to the channel state, folding both counters into a single listener so one event produces one state update. Co-authored-by: Claude Opus 5 (cherry picked from commit 9610ce2f88f8fe8c2290a1d0fbf7c0bcdcc4031b) --- packages/stream_chat/CHANGELOG.md | 8 + .../stream_chat/lib/src/client/channel.dart | 12 +- .../lib/src/core/models/event.dart | 11 + .../lib/src/core/models/event.g.dart | 3 + packages/stream_chat/test/fixtures/event.json | 3 +- .../test/src/client/channel_test.dart | 205 ++++++++++++++++++ .../test/src/core/models/event_test.dart | 6 + 7 files changed, 244 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index ff35082b85..48337dd8bf 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,9 +1,17 @@ ## Upcoming +✅ Added + +- Added `Event.channelMemberCount`, exposing the server-provided `channel_member_count` field on channel events (e.g. `member.added`, `member.removed`, `member.updated`). + ⚠️ Deprecated - Deprecated `StreamChatClient.unflagMessage` and `StreamChatClient.unflagUser`. The `/moderation/unflag` endpoint is no longer supported by the server and the calls have no effect; both methods will be removed in a future major release. +🐞 Fixed + +- Fixed `Channel.memberCount` / `memberCountStream` staying stale for the rest of the session after members joined or left; channel events now apply the server-provided member count, the same way `messageCount` already did. + ## 9.28.0 ✅ Added diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 3ed717da9a..510a8138c8 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -2246,7 +2246,7 @@ class ChannelClientState { _listenChannelUpdated(); - _listenChannelMessageCount(); + _listenChannelCounts(); _listenMemberAdded(); @@ -2381,15 +2381,21 @@ class ChannelClientState { })); } - void _listenChannelMessageCount() { + // Most channel events carry the channel's member and message counts as + // event metadata, reflecting the authoritative values after the change. + // Applying them keeps the counts fresh for the whole session instead of + // only right after a `query` / `watch`. + void _listenChannelCounts() { _subscriptions.add(_channel.on().listen( (Event e) { + final memberCount = e.channelMemberCount; final messageCount = e.channelMessageCount; - if (messageCount == null) return; + if (memberCount == null && messageCount == null) return; updateChannelState( channelState.copyWith( channel: channelState.channel?.copyWith( + memberCount: memberCount, messageCount: messageCount, ), ), diff --git a/packages/stream_chat/lib/src/core/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart index 4b1b672691..91788b929d 100644 --- a/packages/stream_chat/lib/src/core/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -43,6 +43,7 @@ class Event { this.reminder, this.pushPreference, this.channelPushPreference, + this.channelMemberCount, this.channelMessageCount, this.watcherCount, this.lastDeliveredAt, @@ -166,6 +167,13 @@ class Event { /// Push notification preferences for the current user for this channel. final ChannelPushPreference? channelPushPreference; + /// The total number of members in the channel. + /// + /// Sent with channel events such as `member.added`, `member.removed` and + /// `member.updated`, reflecting the authoritative member count after the + /// change. + final int? channelMemberCount; + /// The total number of messages in the channel. final int? channelMessageCount; @@ -222,6 +230,7 @@ class Event { 'reminder', 'push_preference', 'channel_push_preference', + 'channel_member_count', 'channel_message_count', 'watcher_count', 'last_delivered_at', @@ -269,6 +278,7 @@ class Event { MessageReminder? reminder, PushPreference? pushPreference, ChannelPushPreference? channelPushPreference, + int? channelMemberCount, int? channelMessageCount, int? watcherCount, DateTime? lastDeliveredAt, @@ -311,6 +321,7 @@ class Event { pushPreference: pushPreference ?? this.pushPreference, channelPushPreference: channelPushPreference ?? this.channelPushPreference, + channelMemberCount: channelMemberCount ?? this.channelMemberCount, channelMessageCount: channelMessageCount ?? this.channelMessageCount, watcherCount: watcherCount ?? this.watcherCount, lastDeliveredAt: lastDeliveredAt ?? this.lastDeliveredAt, diff --git a/packages/stream_chat/lib/src/core/models/event.g.dart b/packages/stream_chat/lib/src/core/models/event.g.dart index 863de9844c..ab7624c818 100644 --- a/packages/stream_chat/lib/src/core/models/event.g.dart +++ b/packages/stream_chat/lib/src/core/models/event.g.dart @@ -76,6 +76,7 @@ Event _$EventFromJson(Map json) => Event( ? null : ChannelPushPreference.fromJson( json['channel_push_preference'] as Map), + channelMemberCount: (json['channel_member_count'] as num?)?.toInt(), channelMessageCount: (json['channel_message_count'] as num?)?.toInt(), watcherCount: (json['watcher_count'] as num?)?.toInt(), lastDeliveredAt: json['last_delivered_at'] == null @@ -130,6 +131,8 @@ Map _$EventToJson(Event instance) => { 'push_preference': value, if (instance.channelPushPreference?.toJson() case final value?) 'channel_push_preference': value, + if (instance.channelMemberCount case final value?) + 'channel_member_count': value, if (instance.channelMessageCount case final value?) 'channel_message_count': value, if (instance.watcherCount case final value?) 'watcher_count': value, diff --git a/packages/stream_chat/test/fixtures/event.json b/packages/stream_chat/test/fixtures/event.json index b164b3aa59..b0429f5490 100644 --- a/packages/stream_chat/test/fixtures/event.json +++ b/packages/stream_chat/test/fixtures/event.json @@ -31,5 +31,6 @@ "unread_thread_messages": 2, "unread_threads": 3, "channel_last_message_at": "2019-03-27T17:40:17.155892Z", - "watcher_count": 12 + "watcher_count": 12, + "channel_member_count": 4 } \ No newline at end of file diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 87ddcbcc13..1909ea8fb0 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -7629,6 +7629,211 @@ void main() { }, ); }); + + group('Channel member count events', () { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + late Channel channel; + + setUp(() { + final channelState = _generateChannelState(channelId, channelType); + channel = Channel.fromState(client, channelState); + }); + + tearDown(() { + channel.dispose(); + }); + + test( + 'should update channel memberCount when event contains channelMemberCount', + () async { + // Verify initial state - default memberCount + expect(channel.memberCount, equals(0)); + + // Create event with channelMemberCount + final memberCountEvent = Event( + cid: channel.cid, + type: EventType.memberAdded, + member: Member( + userId: 'user-1', + user: User(id: 'user-1'), + ), + channelMemberCount: 42, + ); + + // Dispatch event + client.addEvent(memberCountEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Verify channel memberCount was updated + expect(channel.memberCount, equals(42)); + }, + ); + + test( + 'should update channel memberCount from member.added and member.removed events', + () async { + // Test with member.added event - count increases + final memberAddedEvent = Event( + cid: channel.cid, + type: EventType.memberAdded, + member: Member( + userId: 'user-1', + user: User(id: 'user-1'), + ), + channelMemberCount: 1, + ); + + client.addEvent(memberAddedEvent); + await Future.delayed(Duration.zero); + expect(channel.memberCount, equals(1)); + expect(channel.state?.channelState.members?.map((it) => it.userId), + equals(['user-1'])); + + // Test with another member.added event - count increases + final memberAddedEvent2 = Event( + cid: channel.cid, + type: EventType.memberAdded, + member: Member( + userId: 'user-2', + user: User(id: 'user-2'), + ), + channelMemberCount: 2, + ); + + client.addEvent(memberAddedEvent2); + await Future.delayed(Duration.zero); + expect(channel.memberCount, equals(2)); + expect( + channel.state?.channelState.members?.map((it) => it.userId), + equals(['user-1', 'user-2']), + ); + + // Test with member.removed event - count decreases + final memberRemovedEvent = Event( + cid: channel.cid, + type: EventType.memberRemoved, + user: User(id: 'user-1'), + channelMemberCount: 1, + ); + + client.addEvent(memberRemovedEvent); + await Future.delayed(Duration.zero); + expect(channel.memberCount, equals(1)); + expect(channel.state?.channelState.members?.map((it) => it.userId), + equals(['user-2'])); + }, + ); + + test( + 'should preserve other channel properties when updating memberCount', + () async { + // Set initial channel state with some properties + final initialChannel = channel.state?.channelState.channel?.copyWith( + extraData: {'name': 'Test Channel'}, + messageCount: 7, + frozen: true, + ); + + if (initialChannel != null) { + channel.state?.updateChannelState( + channel.state!.channelState.copyWith(channel: initialChannel), + ); + } + + // Verify initial state + expect(channel.name, 'Test Channel'); + expect(channel.messageCount, equals(7)); + expect(channel.frozen, equals(true)); + expect(channel.memberCount, equals(0)); + + // Update memberCount via event + final memberCountEvent = Event( + cid: channel.cid, + type: EventType.memberAdded, + member: Member( + userId: 'user-1', + user: User(id: 'user-1'), + ), + channelMemberCount: 100, + ); + + client.addEvent(memberCountEvent); + await Future.delayed(Duration.zero); + + // Verify memberCount was updated while preserving other properties + expect(channel.memberCount, equals(100)); + expect(channel.name, 'Test Channel'); + expect(channel.messageCount, equals(7)); + expect(channel.frozen, equals(true)); + }, + ); + + test( + 'should not update memberCount when the event omits channelMemberCount', + () async { + // Seed a known member count. + client.addEvent( + Event( + cid: channel.cid, + type: EventType.memberAdded, + member: Member( + userId: 'user-1', + user: User(id: 'user-1'), + ), + channelMemberCount: 5, + ), + ); + + await Future.delayed(Duration.zero); + expect(channel.memberCount, equals(5)); + + // An event without the field should leave the count untouched. + client.addEvent( + Event( + cid: channel.cid, + type: EventType.memberAdded, + member: Member( + userId: 'user-2', + user: User(id: 'user-2'), + ), + ), + ); + + await Future.delayed(Duration.zero); + expect(channel.memberCount, equals(5)); + }, + ); + + test( + 'should provide memberCountStream for reactive updates', + () async { + expectLater( + channel.memberCountStream.distinct(), + emitsInOrder([0, 1, 5, 10]), + ); + + // Update memberCount multiple times + final counts = [1, 5, 10]; + for (final count in counts) { + final event = Event( + cid: channel.cid, + type: EventType.memberAdded, + member: Member( + userId: 'user-$count', + user: User(id: 'user-$count'), + ), + channelMemberCount: count, + ); + + client.addEvent(event); + await Future.delayed(Duration.zero); + } + }, + ); + }); }); group('Channel filterTags', () { diff --git a/packages/stream_chat/test/src/core/models/event_test.dart b/packages/stream_chat/test/src/core/models/event_test.dart index ab5c78d2fd..b430a27ab8 100644 --- a/packages/stream_chat/test/src/core/models/event_test.dart +++ b/packages/stream_chat/test/src/core/models/event_test.dart @@ -20,6 +20,7 @@ void main() { expect(event.unreadThreads, 3); expect(event.channelLastMessageAt, isA()); expect(event.watcherCount, 12); + expect(event.channelMemberCount, 4); expect(event.lastReadAt, null); expect(event.unreadMessages, null); expect(event.lastReadMessageId, null); @@ -64,6 +65,7 @@ void main() { unreadThreads: 3, channelLastMessageAt: DateTime.parse('2019-03-27T17:40:17.155892Z'), watcherCount: 9, + channelMemberCount: 4, lastReadAt: DateTime.parse('2020-02-10T10:00:00.000Z'), unreadMessages: 5, lastReadMessageId: 'last-read-message-id', @@ -109,6 +111,7 @@ void main() { 'unread_thread_messages': 2, 'unread_threads': 3, 'channel_last_message_at': '2019-03-27T17:40:17.155892Z', + 'channel_member_count': 4, 'watcher_count': 9, 'last_read_at': '2020-02-10T10:00:00.000Z', 'unread_messages': 5, @@ -149,6 +152,7 @@ void main() { expect(newEvent.unreadThreads, 3); expect(newEvent.channelLastMessageAt, isA()); expect(newEvent.watcherCount, 12); + expect(newEvent.channelMemberCount, 4); expect(newEvent.lastReadAt, null); expect(newEvent.unreadMessages, null); expect(newEvent.lastReadMessageId, null); @@ -176,6 +180,7 @@ void main() { unreadThreads: 7, channelLastMessageAt: DateTime.parse('2020-01-29T03:22:47.636130Z'), watcherCount: 21, + channelMemberCount: 8, lastReadAt: DateTime.parse('2020-02-10T10:00:00.000000Z'), unreadMessages: 5, lastReadMessageId: 'last-read-message-id', @@ -202,6 +207,7 @@ void main() { ); expect(newEvent.unreadMessages, 5); expect(newEvent.watcherCount, 21); + expect(newEvent.channelMemberCount, 8); expect(newEvent.lastReadMessageId, 'last-read-message-id'); expect(newEvent.draft, isNotNull); expect(newEvent.draft, equals(draft)); From b78f083274a66670102c3bc7bff7a0733e14ba69 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Sep 2026 15:08:21 +0200 Subject: [PATCH 2/2] perf(llc): make primitive value streams distinct Channel and ClientState getters that map to a single primitive value now dedupe, so listeners only wake up when the value actually changes rather than on every channel state or current user update. The message input's redundant `distinct()` on the draft stream is dropped, as `ChannelClientState.draftStream` and `threadDraftStream` already dedupe. Port of #2935 from master. Co-Authored-By: Claude Opus 5 (1M context) --- packages/stream_chat/CHANGELOG.md | 4 ++ .../stream_chat/lib/src/client/channel.dart | 54 +++++++++++++------ .../stream_chat/lib/src/client/client.dart | 9 ++-- .../test/src/client/channel_test.dart | 40 ++++++++------ .../message_input/stream_message_input.dart | 2 +- 5 files changed, 72 insertions(+), 37 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 48337dd8bf..a57eb6914b 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -4,6 +4,10 @@ - Added `Event.channelMemberCount`, exposing the server-provided `channel_member_count` field on channel events (e.g. `member.added`, `member.removed`, `member.updated`). +🔄 Changed + +- `Channel` and `ClientState` streams that expose a single primitive value are now distinct, so they only emit when the value actually changes. Affects `Channel.memberCountStream`, `messageCountStream`, `watcherCountStream`, `cooldownStream`, `nameStream`, `imageStream`, `frozenStream`, `disabledStream`, `hiddenStream`, `isPinnedStream`, `isArchivedStream`, `createdAtStream`, `updatedAtStream`, `deletedAtStream`, `truncatedAtStream`, `lastMessageAtStream`, and `ClientState.totalUnreadCountStream`, `unreadChannelsStream`, `unreadThreadsStream`. + ⚠️ Deprecated - Deprecated `StreamChatClient.unflagMessage` and `StreamChatClient.unflagUser`. The `/moderation/unflag` endpoint is no longer supported by the server and the calls have no effect; both methods will be removed in a future major release. diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 510a8138c8..5acf7bea6c 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -207,7 +207,9 @@ class Channel { /// Channel frozen status as a stream. Stream get frozenStream { _checkInitialized(); - return state!.channelStateStream.map((cs) => cs.channel?.frozen == true); + return state!.channelStateStream + .map((cs) => cs.channel?.frozen == true) + .distinct(); } /// Channel disabled status. @@ -219,7 +221,9 @@ class Channel { /// Channel disabled status as a stream. Stream get disabledStream { _checkInitialized(); - return state!.channelStateStream.map((cs) => cs.channel?.disabled == true); + return state!.channelStateStream + .map((cs) => cs.channel?.disabled == true) + .distinct(); } /// Channel hidden status. @@ -231,7 +235,9 @@ class Channel { /// Channel hidden status as a stream. Stream get hiddenStream { _checkInitialized(); - return state!.channelStateStream.map((cs) => cs.channel?.hidden == true); + return state!.channelStateStream + .map((cs) => cs.channel?.hidden == true) + .distinct(); } /// Channel pinned status. @@ -244,7 +250,7 @@ class Channel { /// Channel pinned status as a stream. /// Status is specific to the current user. Stream get isPinnedStream { - return membershipStream.map((m) => m?.pinnedAt != null); + return membershipStream.map((m) => m?.pinnedAt != null).distinct(); } /// Channel archived status. @@ -257,7 +263,7 @@ class Channel { /// Channel archived status as a stream. /// Status is specific to the current user. Stream get isArchivedStream { - return membershipStream.map((m) => m?.archivedAt != null); + return membershipStream.map((m) => m?.archivedAt != null).distinct(); } /// The last date at which the channel got truncated. @@ -269,7 +275,9 @@ class Channel { /// The last date at which the channel got truncated as a stream. Stream get truncatedAtStream { _checkInitialized(); - return state!.channelStateStream.map((cs) => cs.channel?.truncatedAt); + return state!.channelStateStream + .map((cs) => cs.channel?.truncatedAt) + .distinct(); } /// Cooldown count @@ -281,7 +289,9 @@ class Channel { /// Cooldown count as a stream Stream get cooldownStream { _checkInitialized(); - return state!.channelStateStream.map((cs) => cs.channel?.cooldown ?? 0); + return state!.channelStateStream + .map((cs) => cs.channel?.cooldown ?? 0) + .distinct(); } /// Remaining cooldown duration in seconds for the channel. @@ -322,7 +332,9 @@ class Channel { /// Channel creation date as a stream. Stream get createdAtStream { _checkInitialized(); - return state!.channelStateStream.map((cs) => cs.channel?.createdAt); + return state!.channelStateStream + .map((cs) => cs.channel?.createdAt) + .distinct(); } /// Channel last message date. @@ -334,7 +346,9 @@ class Channel { /// Channel last message date as a stream. Stream get lastMessageAtStream { _checkInitialized(); - return state!.channelStateStream.map((cs) => cs.channel?.lastMessageAt); + return state!.channelStateStream + .map((cs) => cs.channel?.lastMessageAt) + .distinct(); } DateTime? _currentUserLastMessageAt(List? messages) { @@ -387,7 +401,9 @@ class Channel { /// Channel updated date as a stream. Stream get updatedAtStream { _checkInitialized(); - return state!.channelStateStream.map((cs) => cs.channel?.updatedAt); + return state!.channelStateStream + .map((cs) => cs.channel?.updatedAt) + .distinct(); } /// Channel deletion date. @@ -399,7 +415,9 @@ class Channel { /// Channel deletion date as a stream. Stream get deletedAtStream { _checkInitialized(); - return state!.channelStateStream.map((cs) => cs.channel?.deletedAt); + return state!.channelStateStream + .map((cs) => cs.channel?.deletedAt) + .distinct(); } /// Channel member count. @@ -411,7 +429,9 @@ class Channel { /// Channel member count as a stream. Stream get memberCountStream { _checkInitialized(); - return state!.channelStateStream.map((cs) => cs.channel?.memberCount); + return state!.channelStateStream + .map((cs) => cs.channel?.memberCount) + .distinct(); } /// Channel message count. @@ -429,7 +449,9 @@ class Channel { /// enabled for your app. Stream get messageCountStream { _checkInitialized(); - return state!.channelStateStream.map((cs) => cs.channel?.messageCount); + return state!.channelStateStream + .map((cs) => cs.channel?.messageCount) + .distinct(); } /// List of filter tags applied to this channel. @@ -496,7 +518,7 @@ class Channel { /// {@macro name} Stream get nameStream { _checkInitialized(); - return extraDataStream.map((it) => it['name'] as String?); + return extraDataStream.map((it) => it['name'] as String?).distinct(); } /// Shortcut to get channel image. @@ -511,7 +533,7 @@ class Channel { /// {@macro image} Stream get imageStream { _checkInitialized(); - return extraDataStream.map((it) => it['image'] as String?); + return extraDataStream.map((it) => it['image'] as String?).distinct(); } /// The main Stream chat client. @@ -3271,7 +3293,7 @@ class ChannelClientState { /// Channel watcher count as a stream. Stream get watcherCountStream => - channelStateStream.map((cs) => cs.watcherCount); + channelStateStream.map((cs) => cs.watcherCount).distinct(); /// Channel watchers list. List get watchers => (_channelState.watchers ?? []) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index dfdfb33984..c30f355b44 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -2588,19 +2588,22 @@ class ClientState { int get unreadChannels => _unreadChannelsController.value; /// The current unread channels count as a stream - Stream get unreadChannelsStream => _unreadChannelsController.stream; + Stream get unreadChannelsStream => + _unreadChannelsController.stream.distinct(); /// The current unread thread count. int get unreadThreads => _unreadThreadsController.value; /// The current unread threads count as a stream. - Stream get unreadThreadsStream => _unreadThreadsController.stream; + Stream get unreadThreadsStream => + _unreadThreadsController.stream.distinct(); /// The current total unread messages count int get totalUnreadCount => _totalUnreadCountController.value; /// The current total unread messages count as a stream - Stream get totalUnreadCountStream => _totalUnreadCountController.stream; + Stream get totalUnreadCountStream => + _totalUnreadCountController.stream.distinct(); /// The current list of channels in memory as a stream Stream> get channelsStream => _channelsController.stream; diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 1909ea8fb0..58d19a0acf 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -7604,19 +7604,19 @@ void main() { test( 'should provide messageCountStream for reactive updates', () async { - expectLater( - channel.messageCountStream.distinct(), - emitsInOrder([null, 1, 5, 10]), - ); + final emitted = []; + final subscription = channel.messageCountStream.listen(emitted.add); + addTearDown(subscription.cancel); + await Future.delayed(Duration.zero); - // Update messageCount multiple times - final counts = [1, 5, 10]; - for (final count in counts) { + // Update messageCount multiple times, repeating one of the counts. + final counts = [1, 5, 5, 10]; + for (final (index, count) in counts.indexed) { final event = Event( cid: channel.cid, type: EventType.messageNew, message: Message( - id: 'msg-$count', + id: 'msg-$index', text: 'Message $count', user: User(id: 'user-1'), ), @@ -7626,6 +7626,9 @@ void main() { client.addEvent(event); await Future.delayed(Duration.zero); } + + // The repeated count should not be emitted twice. + expect(emitted, equals([null, 1, 5, 10])); }, ); }); @@ -7810,20 +7813,20 @@ void main() { test( 'should provide memberCountStream for reactive updates', () async { - expectLater( - channel.memberCountStream.distinct(), - emitsInOrder([0, 1, 5, 10]), - ); + final emitted = []; + final subscription = channel.memberCountStream.listen(emitted.add); + addTearDown(subscription.cancel); + await Future.delayed(Duration.zero); - // Update memberCount multiple times - final counts = [1, 5, 10]; - for (final count in counts) { + // Update memberCount multiple times, repeating one of the counts. + final counts = [1, 5, 5, 10]; + for (final (index, count) in counts.indexed) { final event = Event( cid: channel.cid, type: EventType.memberAdded, member: Member( - userId: 'user-$count', - user: User(id: 'user-$count'), + userId: 'user-$index', + user: User(id: 'user-$index'), ), channelMemberCount: count, ); @@ -7831,6 +7834,9 @@ void main() { client.addEvent(event); await Future.delayed(Duration.zero); } + + // The repeated count should not be emitted twice. + expect(emitted, equals([0, 1, 5, 10])); }, ); }); diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart index 7eac9585aa..e9a64f3f84 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart @@ -612,7 +612,7 @@ class StreamMessageInputState extends State _ => channel.state?.draftStream, }; - _draftStreamSubscription = draftStream?.distinct().listen(_onDraftUpdate); + _draftStreamSubscription = draftStream?.listen(_onDraftUpdate); } }