Skip to content

Commit 3c6ad32

Browse files
xsahil03xclaude
andauthored
fix(llc): handle per-channel notification.mark_read events again (#2890)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent cb43fc4 commit 3c6ad32

3 files changed

Lines changed: 174 additions & 1 deletion

File tree

packages/stream_chat/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- Fixed reaction groups synthesized from legacy `reaction_counts`/`reaction_scores` payloads being discarded at parse time when their score total was zero or negative despite a positive count.
3131
- Fixed `Channel.getReplies` adding the parent message to `ChannelClientState.threads` when a backend returns it alongside the replies, which rendered the thread root twice. The online path now filters it out, matching the offline one.
3232
- Fixed truncated channels dropping to the bottom of the list when sorting by `last_updated`.
33+
- Fixed `ChannelClientState` no longer handling `notification.mark_read` (regression since 9.20.0), which left `unreadCount` stale after `Channel.markRead` on channels the user isn't watching.
3334

3435
## 10.2.0
3536

packages/stream_chat/lib/src/client/channel.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3450,7 +3450,7 @@ class ChannelClientState {
34503450
void _listenReadEvents() {
34513451
_subscriptions
34523452
..add(
3453-
_channel.on(EventType.messageRead).listen(
3453+
_channel.on(EventType.messageRead, EventType.notificationMarkRead).listen(
34543454
(event) {
34553455
// Skip handling the event if delivered for a thread
34563456
if (event.thread != null) return;

packages/stream_chat/test/src/client/channel_test.dart

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6812,6 +6812,178 @@ void main() {
68126812
},
68136813
);
68146814

6815+
test(
6816+
'should reset unread count on notification mark read event',
6817+
() async {
6818+
final currentUser = client.state.currentUser!;
6819+
final currentRead = Read(
6820+
user: currentUser,
6821+
lastRead: DateTime(2020),
6822+
unreadMessages: 10,
6823+
);
6824+
6825+
// Setup initial read state
6826+
channel.state?.updateChannelState(
6827+
channel.state!.channelState.copyWith(
6828+
read: [currentRead],
6829+
),
6830+
);
6831+
6832+
when(
6833+
() => client.channelDeliveryReporter.reconcileDelivery([channel]),
6834+
).thenAnswer((_) => Future.value());
6835+
6836+
// Verify initial state
6837+
expect(channel.state?.unreadCount, 10);
6838+
6839+
// notification.mark_read is delivered on the reading user's own
6840+
// connection, so it reaches non-watched channels as well.
6841+
client.addEvent(
6842+
Event(
6843+
cid: channel.cid,
6844+
type: EventType.notificationMarkRead,
6845+
user: currentUser,
6846+
createdAt: DateTime(2022),
6847+
lastReadMessageId: 'message-123',
6848+
),
6849+
);
6850+
6851+
// Wait for event to be processed
6852+
await Future.delayed(Duration.zero);
6853+
6854+
// Verify read state is updated
6855+
final updatedRead = channel.state?.read.first;
6856+
expect(updatedRead?.user.id, currentUser.id);
6857+
expect(channel.state?.unreadCount, 0);
6858+
expect(updatedRead?.lastReadMessageId, 'message-123');
6859+
expect(
6860+
updatedRead?.lastRead.isAtSameMomentAs(DateTime(2022)),
6861+
isTrue,
6862+
);
6863+
},
6864+
);
6865+
6866+
test(
6867+
'should preserve delivery info on notification mark read event',
6868+
() async {
6869+
final currentUser = User(id: 'test-user');
6870+
final currentRead = Read(
6871+
user: currentUser,
6872+
lastRead: DateTime(2020),
6873+
unreadMessages: 10,
6874+
lastDeliveredAt: DateTime(2021),
6875+
lastDeliveredMessageId: 'delivered-msg-456',
6876+
);
6877+
6878+
// Setup initial read state
6879+
channel.state?.updateChannelState(
6880+
channel.state!.channelState.copyWith(
6881+
read: [currentRead],
6882+
),
6883+
);
6884+
6885+
client.addEvent(
6886+
Event(
6887+
cid: channel.cid,
6888+
type: EventType.notificationMarkRead,
6889+
user: currentUser,
6890+
createdAt: DateTime(2022),
6891+
lastReadMessageId: 'message-123',
6892+
),
6893+
);
6894+
6895+
// Wait for event to be processed
6896+
await Future.delayed(Duration.zero);
6897+
6898+
// Verify read state is updated but delivery info is preserved
6899+
final updatedRead = channel.state?.read.first;
6900+
expect(updatedRead?.unreadMessages, 0);
6901+
expect(
6902+
updatedRead?.lastDeliveredAt?.isAtSameMomentAs(DateTime(2021)),
6903+
isTrue,
6904+
);
6905+
expect(updatedRead?.lastDeliveredMessageId, 'delivered-msg-456');
6906+
},
6907+
);
6908+
6909+
test(
6910+
'should not update channel read state on thread notification mark '
6911+
'read event',
6912+
() async {
6913+
final currentUser = User(id: 'test-user');
6914+
final currentRead = Read(
6915+
user: currentUser,
6916+
lastRead: DateTime(2020),
6917+
unreadMessages: 10,
6918+
lastReadMessageId: 'channel-msg-1',
6919+
);
6920+
6921+
// Setup initial read state
6922+
channel.state?.updateChannelState(
6923+
channel.state!.channelState.copyWith(
6924+
read: [currentRead],
6925+
),
6926+
);
6927+
6928+
client.addEvent(
6929+
Event(
6930+
cid: channel.cid,
6931+
type: EventType.notificationMarkRead,
6932+
user: currentUser,
6933+
createdAt: DateTime(2022),
6934+
lastReadMessageId: 'thread-reply-99',
6935+
thread: Thread(
6936+
channelCid: channel.cid!,
6937+
parentMessageId: 'parent-msg-1',
6938+
createdByUserId: currentUser.id,
6939+
replyCount: 3,
6940+
participantCount: 2,
6941+
),
6942+
),
6943+
);
6944+
6945+
// Wait for event to be processed
6946+
await Future.delayed(Duration.zero);
6947+
6948+
// Channel read state must be untouched — thread reads
6949+
// must not clobber the channel-level Read.
6950+
final after = channel.state?.read.first;
6951+
expect(after?.unreadMessages, 10);
6952+
expect(after?.lastReadMessageId, 'channel-msg-1');
6953+
expect(after?.lastRead.isAtSameMomentAs(DateTime(2020)), isTrue);
6954+
},
6955+
);
6956+
6957+
test(
6958+
'should reconcile delivery when notification mark read event is from '
6959+
'current user',
6960+
() async {
6961+
final currentUser = client.state.currentUser;
6962+
6963+
when(
6964+
() => client.channelDeliveryReporter.reconcileDelivery([channel]),
6965+
).thenAnswer((_) => Future.value());
6966+
6967+
client.addEvent(
6968+
Event(
6969+
cid: channel.cid,
6970+
type: EventType.notificationMarkRead,
6971+
user: currentUser,
6972+
createdAt: DateTime(2022),
6973+
lastReadMessageId: 'message-123',
6974+
),
6975+
);
6976+
6977+
// Wait for event to be processed
6978+
await Future.delayed(Duration.zero);
6979+
6980+
// Verify reconcileDelivery was called
6981+
verify(
6982+
() => client.channelDeliveryReporter.reconcileDelivery([channel]),
6983+
).called(1);
6984+
},
6985+
);
6986+
68156987
test('should update read state on message delivered event', () async {
68166988
final currentUser = User(id: 'test-user');
68176989
final distantPast = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);

0 commit comments

Comments
 (0)