From e56ee12c2bd976d05020f753f76c5afb797223f1 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Jul 2025 14:44:21 +0200 Subject: [PATCH 1/3] fix(llc, core): retain cached channel messages on offline access with unread messages --- packages/stream_chat/lib/src/client/channel.dart | 13 ++++++++++++- packages/stream_chat/lib/src/core/api/requests.dart | 5 +++++ .../lib/src/stream_channel.dart | 2 -- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index de20218bc..d92975490 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1776,7 +1776,18 @@ class Channel { if (this.state == null) { _initState(channelState); } else { - // Otherwise, update the channel state. + // Otherwise, we update the existing state with the new channel state. + // + // But, before updating the state, we check if we are querying around a + // message, If we are, we have to truncate the state to avoid potential + // gaps in the message sequence. + final isQueryingAround = switch (messagesPagination) { + PaginationParams(idAround: _?) => true, + PaginationParams(createdAtAround: _?) => true, + _ => false, + }; + + if (isQueryingAround) this.state?.truncate(); this.state?.updateChannelState(channelState); } diff --git a/packages/stream_chat/lib/src/core/api/requests.dart b/packages/stream_chat/lib/src/core/api/requests.dart index 141df822e..0b5a70460 100644 --- a/packages/stream_chat/lib/src/core/api/requests.dart +++ b/packages/stream_chat/lib/src/core/api/requests.dart @@ -137,6 +137,11 @@ class PaginationParams extends Equatable { greaterThanOrEqual, lessThan, lessThanOrEqual, + createdAtAfterOrEqual, + createdAtAfter, + createdAtBeforeOrEqual, + createdAtBefore, + createdAtAround, ]; } diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index e8fe35070..015f92084 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -358,7 +358,6 @@ class StreamChannelState extends State { }) async { if (channel.state == null) return null; channel.state?.isUpToDate = false; - channel.state?.truncate(); final pagination = PaginationParams( limit: limit, @@ -461,7 +460,6 @@ class StreamChannelState extends State { }) async { if (channel.state == null) return null; channel.state?.isUpToDate = false; - channel.state?.truncate(); final pagination = PaginationParams( limit: limit, From 02b2cbc3e0dc0887b0d53c6daace379d707214e8 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Jul 2025 14:46:04 +0200 Subject: [PATCH 2/3] chore: update CHANGELOG.md --- packages/stream_chat/CHANGELOG.md | 5 +++++ packages/stream_chat_flutter_core/CHANGELOG.md | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 1fc90ed7d..f5c8bcc01 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,5 +1,10 @@ ## Upcoming +🐞 Fixed + +- Fixed cached messages are cleared from channels with unread messages when accessed + offline. [[#2083]](https://github.com/GetStream/stream-chat-flutter/issues/2083) + 🔄 Changed - Deprecated `SortOption.new` constructor in favor of `SortOption.desc` and `SortOption.asc`. diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 9978e7aa3..e31959bf0 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,10 @@ +## Upcoming + +🐞 Fixed + +- Fixed cached messages are cleared from channels with unread messages when accessed + offline. [[#2083]](https://github.com/GetStream/stream-chat-flutter/issues/2083) + ## 9.13.0 🐞 Fixed From e2a1358090fbd047e39976fec3e9dbccc13a24bf Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Jul 2025 15:16:42 +0200 Subject: [PATCH 3/3] test: add tests --- .../test/src/client/channel_test.dart | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 13480a595..fb011ded7 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -2749,6 +2749,121 @@ void main() { ), ).called(1); }); + + test('should truncate state when querying around message id', () async { + final initialMessages = [ + Message(id: 'msg1', text: 'Hello 1'), + Message(id: 'msg2', text: 'Hello 2'), + Message(id: 'msg3', text: 'Hello 3'), + ]; + + final stateWithMessages = _generateChannelState( + channelId, + channelType, + ).copyWith(messages: initialMessages); + + channel.state!.updateChannelState(stateWithMessages); + expect(channel.state!.messages, hasLength(3)); + + final newState = _generateChannelState( + channelId, + channelType, + ).copyWith(messages: [ + Message(id: 'msg-before-1', text: 'Message before 1'), + Message(id: 'msg-before-2', text: 'Message before 2'), + Message(id: 'target-message-id', text: 'Target message'), + Message(id: 'msg-after-1', text: 'Message after 1'), + Message(id: 'msg-after-2', text: 'Message after 2'), + ]); + + when( + () => client.queryChannel( + channelType, + channelId: channelId, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).thenAnswer((_) async => newState); + + const pagination = PaginationParams(idAround: 'target-message-id'); + + final res = await channel.query(messagesPagination: pagination); + + expect(res, isNotNull); + expect(channel.state!.messages, hasLength(5)); + expect(channel.state!.messages[2].id, 'target-message-id'); + + verify( + () => client.queryChannel( + channelType, + channelId: channelId, + channelData: any(named: 'channelData'), + messagesPagination: pagination, + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).called(1); + }); + + test('should truncate state when querying around created date', () async { + final initialMessages = [ + Message(id: 'msg1', text: 'Hello 1'), + Message(id: 'msg2', text: 'Hello 2'), + Message(id: 'msg3', text: 'Hello 3'), + ]; + + final stateWithMessages = _generateChannelState( + channelId, + channelType, + ).copyWith(messages: initialMessages); + + channel.state!.updateChannelState(stateWithMessages); + expect(channel.state!.messages, hasLength(3)); + + final targetDate = DateTime.now(); + final newState = _generateChannelState( + channelId, + channelType, + ).copyWith(messages: [ + Message(id: 'msg-before-1', text: 'Message before 1'), + Message(id: 'msg-before-2', text: 'Message before 2'), + Message(id: 'target-message', text: 'Target message'), + Message(id: 'msg-after-1', text: 'Message after 1'), + Message(id: 'msg-after-2', text: 'Message after 2'), + ]); + + when( + () => client.queryChannel( + channelType, + channelId: channelId, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).thenAnswer((_) async => newState); + + final pagination = PaginationParams(createdAtAround: targetDate); + + final res = await channel.query(messagesPagination: pagination); + + expect(res, isNotNull); + expect(channel.state!.messages, hasLength(5)); + expect(channel.state!.messages[2].id, 'target-message'); + + verify( + () => client.queryChannel( + channelType, + channelId: channelId, + channelData: any(named: 'channelData'), + messagesPagination: pagination, + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).called(1); + }); }); test('`.queryMembers`', () async {