diff --git a/e2e_app/integration_test/draft_messages_test.dart b/e2e_app/integration_test/draft_messages_test.dart index 1d4a21043e..040b40377d 100644 --- a/e2e_app/integration_test/draft_messages_test.dart +++ b/e2e_app/integration_test/draft_messages_test.dart @@ -86,7 +86,6 @@ void main() { streamTestWithEnv( allureId: '12694', description: 'user updates a draft message while offline', - skip: 'https://linear.app/stream/issue/FLU-1086', body: (env) async { step('GIVEN the user opens a channel'); await env.userRobot.login().openChannel(); diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 0a850f8f1c..b19ae2519f 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,10 @@ +## Upcoming + +🐞 Fixed + +- Fixed a draft saved or deleted while offline not being kept. +- Fixed a thread draft replacing the channel's draft when the thread's parent message isn't loaded. + ## 10.6.0 ✅ Added diff --git a/packages/stream_chat/lib/src/client/channel/channel.dart b/packages/stream_chat/lib/src/client/channel/channel.dart index 1d162c5daa..cd29cb61c3 100644 --- a/packages/stream_chat/lib/src/client/channel/channel.dart +++ b/packages/stream_chat/lib/src/client/channel/channel.dart @@ -1294,10 +1294,19 @@ class Channel { ); /// Creates or updates a new [draft] for this channel. + /// + /// Updates the draft in [state] right away, even if the request fails. Future createDraft( DraftMessage draft, ) { _checkInitialized(); + final localDraft = Draft( + channelCid: cid!, + createdAt: DateTime.now(), + message: draft, + parentId: draft.parentId, + ); + state!.updateDraft(localDraft); return _client.createDraft(draft, id!, type); } @@ -1315,10 +1324,17 @@ class Channel { /// /// Optionally, provide a [parentId] to delete the draft for a specific /// thread. + /// + /// Removes the draft from [state], even if the request fails. Future deleteDraft({ String? parentId, }) { _checkInitialized(); + final draft = switch (parentId) { + final parentId? => state!.threadDraft(parentId), + null => state!.draft, + }; + if (draft != null) state!.deleteDraft(draft); return _client.deleteDraft(id!, type, parentId: parentId); } diff --git a/packages/stream_chat/lib/src/client/channel/channel_client_state.dart b/packages/stream_chat/lib/src/client/channel/channel_client_state.dart index f544b97d49..524ba55c30 100644 --- a/packages/stream_chat/lib/src/client/channel/channel_client_state.dart +++ b/packages/stream_chat/lib/src/client/channel/channel_client_state.dart @@ -198,14 +198,15 @@ class ChannelClientState { ); } - /// Updates the [draft] in the channel state or the message if it exists. + /// Updates the [draft] in the channel state, or on its thread's parent + /// message for a thread draft. + /// + /// A thread draft whose parent message isn't loaded is left out. void updateDraft(Draft draft) { if (draft.parentId case final parentId?) { - for (final message in messages) { - if (message.id == parentId) { - return updateMessage(message.copyWith(draft: draft)); - } - } + final parent = messages.firstWhereOrNull((it) => it.id == parentId); + if (parent == null) return; + return updateMessage(parent.copyWith(draft: draft)); } updateChannelState( @@ -224,13 +225,9 @@ class ChannelClientState { ); if (draft.parentId case final parentId?) { - for (final message in messages) { - if (message.id == parentId) { - return updateMessage( - message.copyWith(draft: null), - ); - } - } + final parent = messages.firstWhereOrNull((it) => it.id == parentId); + if (parent == null) return; + return updateMessage(parent.copyWith(draft: null)); } updateChannelState( diff --git a/packages/stream_chat/test/src/client/channel/channel_test.dart b/packages/stream_chat/test/src/client/channel/channel_test.dart index 00007b7146..a26806f892 100644 --- a/packages/stream_chat/test/src/client/channel/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel/channel_test.dart @@ -1226,6 +1226,56 @@ void main() { }); }); + group('`.createDraft` while offline', () { + final draftMessage = DraftMessage(text: 'Typed while offline'); + + setUp(() { + when( + () => client.createDraft(draftMessage, channelId, channelType), + ).thenAnswer((_) async => throw StreamChatNetworkError(ChatErrorCode.internalSystemError)); + }); + + test('keeps the draft in the channel state', () async { + await expectLater(() => channel.createDraft(draftMessage), throwsA(isA())); + + expect(channel.state!.draft?.message, draftMessage); + }); + + test('leaves the channel draft alone for a thread whose parent is not loaded', () async { + final threadDraft = DraftMessage(text: 'Thread reply', parentId: 'unloaded-parent-id'); + when( + () => client.createDraft(threadDraft, channelId, channelType), + ).thenAnswer((_) async => throw StreamChatNetworkError(ChatErrorCode.internalSystemError)); + + await expectLater(() => channel.createDraft(threadDraft), throwsA(isA())); + + expect(channel.state!.draft, isNull); + }); + }); + + group('`.deleteDraft` while offline', () { + setUp(() { + when( + () => client.deleteDraft(channelId, channelType, parentId: any(named: 'parentId')), + ).thenAnswer((_) async => throw StreamChatNetworkError(ChatErrorCode.internalSystemError)); + }); + + test('removes the draft from the channel state', () async { + channel.state!.updateDraft( + Draft( + channelCid: channelCid, + createdAt: DateTime.now(), + message: DraftMessage(text: 'Old draft'), + ), + ); + + await expectLater(() => channel.deleteDraft(), throwsA(isA())); + await Future.delayed(Duration.zero); + + expect(channel.state!.draft, isNull); + }); + }); + group('`.getDraft`', () { final draftMessage = DraftMessage(text: 'Draft message text');