diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 787aa2af..c9ffa7c1 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🐞 Fixed + +- [[#890]](https://github.com/GetStream/stream-chat-flutter/pull/890). Fixed Reactions not updating on thread messages. Thanks [bstolinski](https://github.com/bstolinski). +- ## 3.4.0 🐞 Fixed diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 471f39e6..141e537f 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1695,9 +1695,8 @@ class ChannelClientState { _subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) { final oldMessage = messages.firstWhereOrNull((it) => it.id == event.message?.id) ?? - threads.values - .expand((e) => e) - .firstWhereOrNull((e) => e.id == event.message?.id); + threads[event.message?.parentId] + ?.firstWhereOrNull((e) => e.id == event.message?.id); final reaction = event.reaction; final ownReactions = oldMessage?.ownReactions ?.whereNot((it) => @@ -1718,9 +1717,8 @@ class ChannelClientState { _subscriptions.add(_channel.on(EventType.reactionNew).listen((event) { final oldMessage = messages.firstWhereOrNull((it) => it.id == event.message?.id) ?? - threads.values - .expand((e) => e) - .firstWhereOrNull((e) => e.id == event.message?.id); + threads[event.message?.parentId] + ?.firstWhereOrNull((e) => e.id == event.message?.id); final message = event.message!.copyWith( ownReactions: oldMessage?.ownReactions, ); @@ -1736,8 +1734,9 @@ class ChannelClientState { ) .listen((event) { final oldMessage = - messages.firstWhereOrNull((it) => it.id == event.message?.id); - + messages.firstWhereOrNull((it) => it.id == event.message?.id) ?? + threads[event.message?.parentId] + ?.firstWhereOrNull((e) => e.id == event.message?.id); final message = event.message!.copyWith( ownReactions: oldMessage?.ownReactions, ); diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index adbfb388..50786de0 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -1239,6 +1239,189 @@ void main() { ); }); + group('`.sendReaction in thread`', () { + test('should work fine', () async { + const type = 'test-reaction-type'; + final message = Message( + id: 'test-message-id', + parentId: 'test-parent-id', // is thread message + ); + + final reaction = Reaction(type: type, messageId: message.id); + + when(() => client.sendReaction(message.id, type)).thenAnswer( + (_) async => SendReactionResponse() + ..message = message + ..reaction = reaction, + ); + + expectLater( + channel.state?.threadsStream + // skipping first seed message list -> [] messages + .skip(1) + .map((event) => event['test-parent-id']), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sent, + reactionCounts: {type: 1}, + reactionScores: {type: 1}, + latestReactions: [reaction], + ownReactions: [reaction], + ), + matchReactions: true, + matchSendingStatus: true, + matchParentId: true, + ), + ], + ]), + ); + + final res = await channel.sendReaction(message, type); + + expect(res, isNotNull); + expect(res.reaction.type, type); + expect(res.reaction.messageId, message.id); + + verify(() => client.sendReaction(message.id, type)).called(1); + }); + + test( + '''should restore previous thread message if `client.sendReaction` throws''', + () async { + const type = 'test-reaction-type'; + final message = Message( + id: 'test-message-id', + parentId: 'test-parent-id', // is thread message + ); + + final reaction = Reaction(type: type, messageId: message.id); + + when(() => client.sendReaction(message.id, type)) + .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.threadsStream + .skip(1) + .map((event) => event['test-parent-id']), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sent, + reactionCounts: {type: 1}, + reactionScores: {type: 1}, + latestReactions: [reaction], + ownReactions: [reaction], + ), + matchReactions: true, + matchSendingStatus: true, + matchParentId: true, + ), + ], + [ + isSameMessageAs( + message, + matchReactions: true, + matchSendingStatus: true, + matchParentId: true, + ), + ], + ]), + ); + + try { + await channel.sendReaction(message, type); + } catch (e) { + expect(e, isA()); + } + + verify(() => client.sendReaction(message.id, type)).called(1); + }, + ); + + test( + '''should override previous thread reaction if present and `enforceUnique` is true''', + () async { + const userId = 'test-user-id'; + const messageId = 'test-message-id'; + const parentId = 'test-parent-id'; + const prevType = 'test-reaction-type'; + final prevReaction = Reaction( + type: prevType, + messageId: messageId, + userId: userId, + ); + final message = Message( + id: messageId, + parentId: parentId, + ownReactions: [prevReaction], + latestReactions: [prevReaction], + reactionScores: const {prevType: 1}, + reactionCounts: const {prevType: 1}, + ); + + const type = 'test-reaction-type-2'; + final newReaction = Reaction( + type: type, + messageId: messageId, + userId: userId, + ); + final newMessage = message.copyWith( + ownReactions: [newReaction], + latestReactions: [newReaction], + ); + + const enforceUnique = true; + + when(() => client.sendReaction( + messageId, + type, + enforceUnique: enforceUnique, + )).thenAnswer( + (_) async => SendReactionResponse() + ..message = newMessage + ..reaction = newReaction, + ); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.threadsStream + .skip(1) + .map((event) => event['test-parent-id']), + emitsInOrder([ + [ + isSameMessageAs( + newMessage.copyWith(status: MessageSendingStatus.sent), + matchReactions: true, + matchSendingStatus: true, + matchParentId: true, + ), + ], + ]), + ); + + final res = await channel.sendReaction( + message, + type, + enforceUnique: enforceUnique, + ); + + expect(res, isNotNull); + expect(res.reaction.type, type); + expect(res.reaction.messageId, messageId); + + verify(() => client.sendReaction( + messageId, + type, + enforceUnique: enforceUnique, + )).called(1); + }, + ); + }); + group('`.deleteReaction`', () { test('should work fine', () async { const userId = 'test-user-id'; @@ -1343,6 +1526,121 @@ void main() { ); }); + group('`.deleteReaction in thread`', () { + test('should work fine', () async { + const userId = 'test-user-id'; + const messageId = 'test-message-id'; + const parentId = 'test-parent-id'; + const type = 'test-reaction-type'; + final reaction = Reaction( + type: type, + messageId: messageId, + userId: userId, + ); + final message = Message( + id: messageId, + parentId: parentId, // is thread + ownReactions: [reaction], + latestReactions: [reaction], + reactionScores: const {type: 1}, + reactionCounts: const {type: 1}, + ); + + when(() => client.deleteReaction(messageId, type)) + .thenAnswer((_) async => EmptyResponse()); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.threadsStream + .skip(1) + .map((event) => event['test-parent-id']), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sent, + latestReactions: [], + ownReactions: [], + ), + matchReactions: true, + matchSendingStatus: true, + matchParentId: true, + ), + ], + ]), + ); + + final res = await channel.deleteReaction(message, reaction); + + expect(res, isNotNull); + + verify(() => client.deleteReaction(messageId, type)).called(1); + }); + + test( + 'should restore prev message state if `client.deleteReaction` throws', + () async { + const userId = 'test-user-id'; + const messageId = 'test-message-id'; + const parentId = 'test-parent-id'; + const type = 'test-reaction-type'; + final reaction = Reaction( + type: type, + messageId: messageId, + userId: userId, + ); + final message = Message( + id: messageId, + parentId: parentId, + ownReactions: [reaction], + latestReactions: [reaction], + reactionScores: const {type: 1}, + reactionCounts: const {type: 1}, + ); + + when(() => client.deleteReaction(messageId, type)) + .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.threadsStream + .skip(1) + .map((event) => event['test-parent-id']), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sent, + latestReactions: [], + ownReactions: [], + ), + matchReactions: true, + matchSendingStatus: true, + matchParentId: true, + ), + ], + [ + isSameMessageAs( + message, + matchReactions: true, + matchSendingStatus: true, + matchParentId: true, + ), + ], + ]), + ); + + try { + await channel.deleteReaction(message, reaction); + } catch (e) { + expect(e, isA()); + } + + verify(() => client.deleteReaction(messageId, type)).called(1); + }, + ); + }); + test('`.update`', () async { const channelData = { 'name': 'Stream Team', diff --git a/packages/stream_chat/test/src/matchers.dart b/packages/stream_chat/test/src/matchers.dart index e54b113d..102480cd 100644 --- a/packages/stream_chat/test/src/matchers.dart +++ b/packages/stream_chat/test/src/matchers.dart @@ -49,6 +49,7 @@ Matcher isSameMessageAs( bool matchSendingStatus = false, bool matchAttachments = false, bool matchAttachmentsUploadState = false, + bool matchParentId = false, }) => _IsSameMessageAs( targetMessage: targetMessage, @@ -57,6 +58,7 @@ Matcher isSameMessageAs( matchSendingStatus: matchSendingStatus, matchAttachments: matchAttachments, matchAttachmentsUploadState: matchAttachmentsUploadState, + matchParentId: matchParentId, ); class _IsSameMessageAs extends Matcher { @@ -67,6 +69,7 @@ class _IsSameMessageAs extends Matcher { this.matchSendingStatus = false, this.matchAttachments = false, this.matchAttachmentsUploadState = false, + this.matchParentId = false, }); final Message targetMessage; @@ -75,6 +78,7 @@ class _IsSameMessageAs extends Matcher { final bool matchSendingStatus; final bool matchAttachments; final bool matchAttachmentsUploadState; + final bool matchParentId; @override Description describe(Description description) => @@ -123,6 +127,9 @@ class _IsSameMessageAs extends Matcher { matches &= matchAttachments(); } + if (matchParentId) { + matches &= message.parentId == targetMessage.parentId; + } return matches; } }