From c5d770c7831bfd09b4b65fc1cda28c8ebcdd0c1b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 2 Feb 2021 16:18:00 +0530 Subject: [PATCH 1/7] [LLC] Add optimistic reaction update Signed-off-by: Sahil Kumar --- packages/stream_chat/lib/src/api/channel.dart | 103 ++++++++++++++++-- .../stream_chat/lib/src/models/reaction.dart | 40 ++++++- 2 files changed, 129 insertions(+), 14 deletions(-) diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index 6117b5d7..e5bf528c 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -274,28 +274,107 @@ class Channel { bool enforceUnique = false, }) async { final messageId = message.id; + final now = DateTime.now(); + final user = _client.state.user; + + final latestReactions = [...message.latestReactions]; + final ownReactions = [...message.ownReactions]; + if (enforceUnique) { + latestReactions.removeWhere((it) => it.userId == user.id); + ownReactions.removeWhere((it) => it.userId == user.id); + } + + final newReaction = Reaction( + messageId: messageId, + createdAt: now, + type: type, + user: user, + score: 1, + extraData: extraData, + ); + + // Inserting at the 0th index as it's the latest reaction + latestReactions.insert(0, newReaction); + ownReactions.add(newReaction); + + final newMessage = message.copyWith( + reactionCounts: {...message.reactionCounts}..update(type, (value) { + if (enforceUnique) return value; + return value + 1; + }, ifAbsent: () => 1), + reactionScores: {...message.reactionScores}..update(type, (value) { + if (enforceUnique) return value; + return value + 1; + }, ifAbsent: () => 1), + latestReactions: latestReactions, + ownReactions: ownReactions, + ); + + state?.addMessage(newMessage); + final data = Map.from(extraData) ..addAll({ 'type': type, }); - final res = await _client.post( - '/messages/$messageId/reaction', - data: { - 'reaction': data, - 'enforce_unique': enforceUnique, - }, - ); - return _client.decode(res.data, SendReactionResponse.fromJson); + try { + final res = await _client.post( + '/messages/$messageId/reaction', + data: { + 'reaction': data, + 'enforce_unique': enforceUnique, + }, + ); + final reactionResp = + _client.decode(res.data, SendReactionResponse.fromJson); + state?.addMessage(reactionResp.message); + return reactionResp; + } catch (error) { + if (error is DioError && error.type != DioErrorType.RESPONSE) { + // Reset the message if the update fails + state?.addMessage(message); + } + rethrow; + } } /// Delete a reaction from this channel - Future deleteReaction(Message message, Reaction reaction) { + Future deleteReaction( + Message message, Reaction reaction) async { _checkInitialized(); - return client - .delete('/messages/${message.id}/reaction/${reaction.type}') - .then((res) => _client.decode(res.data, EmptyResponse.fromJson)); + final type = reaction.type; + + final reactionCounts = {...message.reactionCounts} + ..update(type, (value) => value - 1); + final reactionScores = {...message.reactionScores} + ..update(type, (value) => value - 1); + + final removeWhere = (Reaction r) => + r.userId == reaction.userId && + r.type == reaction.type && + r.messageId == reaction.messageId; + + final newMessage = message.copyWith( + reactionCounts: reactionCounts..removeWhere((_, value) => value == 0), + reactionScores: reactionScores..removeWhere((_, value) => value == 0), + latestReactions: [...message.latestReactions]..removeWhere(removeWhere), + ownReactions: [...message.ownReactions]..removeWhere(removeWhere), + ); + + state?.addMessage(newMessage); + + try { + final res = await client + .delete('/messages/${message.id}/reaction/${reaction.type}'); + return _client.decode(res.data, EmptyResponse.fromJson); + } catch (error) { + if (error is DioError && error.type != DioErrorType.RESPONSE) { + // Reset the message if the update fails + state?.addMessage(message); + } + rethrow; + } } /// Edit the channel custom data diff --git a/packages/stream_chat/lib/src/models/reaction.dart b/packages/stream_chat/lib/src/models/reaction.dart index 9a4869f7..f0957761 100644 --- a/packages/stream_chat/lib/src/models/reaction.dart +++ b/packages/stream_chat/lib/src/models/reaction.dart @@ -49,10 +49,10 @@ class Reaction { this.createdAt, this.type, this.user, - this.userId, + String userId, this.score, this.extraData, - }); + }) : userId = userId ?? user?.id; /// Create a new instance from a json factory Reaction.fromJson(Map json) { @@ -65,4 +65,40 @@ class Reaction { return Serialization.moveFromExtraDataToRoot( _$ReactionToJson(this), topLevelFields); } + + /// Creates a copy of [Reaction] with specified attributes overridden. + Reaction copyWith({ + String messageId, + DateTime createdAt, + String type, + User user, + String userId, + int score, + Map extraData, + }) { + return Reaction( + messageId: messageId ?? this.messageId, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + user: user ?? this.user, + userId: userId ?? this.userId, + score: score ?? this.score, + extraData: extraData ?? this.extraData, + ); + } + + /// Returns a new [Reaction] that is a combination of this reaction and the given + /// [other] reaction. + Reaction merge(Reaction other) { + if (other == null) return this; + return copyWith( + messageId: other.messageId, + createdAt: other.createdAt, + type: other.type, + user: other.user, + userId: other.userId, + score: other.score, + extraData: other.extraData, + ); + } } From 94882c73c5bf675b92cfc0843b36197661dbce8d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 2 Feb 2021 16:20:22 +0530 Subject: [PATCH 2/7] [UI-Kit -> MessageReactionsModal] Fix username overflow Signed-off-by: Sahil Kumar --- .../stream_chat_flutter/lib/src/message_reactions_modal.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index e3d4f201..430602cb 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -257,7 +257,7 @@ class MessageReactionsModal extends StatelessWidget { ), const SizedBox(height: 8), Text( - reaction.user.name, + reaction.user.name.split(' ')[0], style: StreamChatTheme.of(context).textTheme.footnoteBold, textAlign: TextAlign.center, ), From 7d02187751ed9583140eca38a7e705aa4a568600 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 2 Feb 2021 16:30:52 +0530 Subject: [PATCH 3/7] [UI-Kit] Fix test Signed-off-by: Sahil Kumar --- .../test/src/message_reaction_modal_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/test/src/message_reaction_modal_test.dart b/packages/stream_chat_flutter/test/src/message_reaction_modal_test.dart index c023b20d..4e515a09 100644 --- a/packages/stream_chat_flutter/test/src/message_reaction_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_reaction_modal_test.dart @@ -96,7 +96,7 @@ void main() { findsNWidgets(2)); expect(find.byKey(Key('StreamSvgIcon-Icon_love_reaction.svg')), findsNWidgets(2)); - expect(find.text(testUserId), findsNWidgets(2)); + expect(find.text(testUserId.split(' ')[0]), findsNWidgets(2)); }, ); } From 0030de3dd6b0dad03f891cc6c8b56f606e807c50 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 2 Feb 2021 17:23:18 +0530 Subject: [PATCH 4/7] [LLC] Fix reactions tests Signed-off-by: Sahil Kumar --- packages/stream_chat/lib/src/api/channel.dart | 30 +++++++++---------- .../test/src/api/channel_test.dart | 21 ++++++------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index e5bf528c..a7034dfd 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -329,11 +329,9 @@ class Channel { _client.decode(res.data, SendReactionResponse.fromJson); state?.addMessage(reactionResp.message); return reactionResp; - } catch (error) { - if (error is DioError && error.type != DioErrorType.RESPONSE) { - // Reset the message if the update fails - state?.addMessage(message); - } + } catch (_) { + // Reset the message if the update fails + state?.addMessage(message); rethrow; } } @@ -341,14 +339,16 @@ class Channel { /// Delete a reaction from this channel Future deleteReaction( Message message, Reaction reaction) async { - _checkInitialized(); - final type = reaction.type; - final reactionCounts = {...message.reactionCounts} - ..update(type, (value) => value - 1); - final reactionScores = {...message.reactionScores} - ..update(type, (value) => value - 1); + final reactionCounts = {...message.reactionCounts}; + if (reactionCounts.containsKey(type)) { + reactionCounts.update(type, (value) => value - 1); + } + final reactionScores = {...message.reactionScores}; + if (reactionScores.containsKey(type)) { + reactionScores.update(type, (value) => value - 1); + } final removeWhere = (Reaction r) => r.userId == reaction.userId && @@ -368,11 +368,9 @@ class Channel { final res = await client .delete('/messages/${message.id}/reaction/${reaction.type}'); return _client.decode(res.data, EmptyResponse.fromJson); - } catch (error) { - if (error is DioError && error.type != DioErrorType.RESPONSE) { - // Reset the message if the update fails - state?.addMessage(message); - } + } catch (_) { + // Reset the message if the update fails + state?.addMessage(message); rethrow; } } diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 4b274a94..9baf4235 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -396,6 +396,10 @@ void main() { await channelClient.sendReaction( Message( id: 'messageid', + reactionCounts: const {}, + reactionScores: const {}, + latestReactions: const [], + ownReactions: const [], ), reactionType, ); @@ -421,20 +425,17 @@ void main() { ); final channelClient = client.channel('messaging', id: 'testid'); - when(mockDio.post( - any, - data: anyNamed('data'), - )).thenAnswer((_) async => Response( - data: '{}', - statusCode: 200, - )); - await channelClient.watch(); - when(mockDio.delete('/messages/messageid/reaction/test')) .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); await channelClient.deleteReaction( - Message(id: 'messageid'), + Message( + id: 'messageid', + reactionCounts: const {}, + reactionScores: const {}, + latestReactions: const [], + ownReactions: const [], + ), Reaction(type: 'test'), ); From c81ff80b4dd2e58be255ae0dfcbedb40842fd170 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 3 Feb 2021 13:18:08 +0530 Subject: [PATCH 5/7] [LLC] Fix reaction events listener Signed-off-by: Sahil Kumar --- packages/stream_chat/lib/src/api/channel.dart | 183 ++++-------------- 1 file changed, 40 insertions(+), 143 deletions(-) diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index a7034dfd..39ed445e 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -277,11 +277,9 @@ class Channel { final now = DateTime.now(); final user = _client.state.user; - final latestReactions = [...message.latestReactions]; - final ownReactions = [...message.ownReactions]; + final latestReactions = [...message.latestReactions ?? []]; if (enforceUnique) { latestReactions.removeWhere((it) => it.userId == user.id); - ownReactions.removeWhere((it) => it.userId == user.id); } final newReaction = Reaction( @@ -295,14 +293,17 @@ class Channel { // Inserting at the 0th index as it's the latest reaction latestReactions.insert(0, newReaction); - ownReactions.add(newReaction); + final ownReactions = [...latestReactions] + ..removeWhere((it) => it.userId != user.id); final newMessage = message.copyWith( - reactionCounts: {...message.reactionCounts}..update(type, (value) { + reactionCounts: {...message?.reactionCounts ?? {}} + ..update(type, (value) { if (enforceUnique) return value; return value + 1; }, ifAbsent: () => 1), - reactionScores: {...message.reactionScores}..update(type, (value) { + reactionScores: {...message.reactionScores ?? {}} + ..update(type, (value) { if (enforceUnique) return value; return value + 1; }, ifAbsent: () => 1), @@ -340,26 +341,32 @@ class Channel { Future deleteReaction( Message message, Reaction reaction) async { final type = reaction.type; + final user = _client.state.user; - final reactionCounts = {...message.reactionCounts}; + final reactionCounts = {...message.reactionCounts ?? {}}; if (reactionCounts.containsKey(type)) { reactionCounts.update(type, (value) => value - 1); } - final reactionScores = {...message.reactionScores}; + final reactionScores = {...message.reactionScores ?? {}}; if (reactionScores.containsKey(type)) { reactionScores.update(type, (value) => value - 1); } - final removeWhere = (Reaction r) => - r.userId == reaction.userId && - r.type == reaction.type && - r.messageId == reaction.messageId; + final latestReactions = [...message.latestReactions ?? []] + ..removeWhere((r) { + return r.userId == reaction.userId && + r.type == reaction.type && + r.messageId == reaction.messageId; + }); + + final ownReactions = [...latestReactions ?? []] + ..removeWhere((it) => it.userId != user.id); final newMessage = message.copyWith( reactionCounts: reactionCounts..removeWhere((_, value) => value == 0), reactionScores: reactionScores..removeWhere((_, value) => value == 0), - latestReactions: [...message.latestReactions]..removeWhere(removeWhere), - ownReactions: [...message.ownReactions]..removeWhere(removeWhere), + latestReactions: latestReactions, + ownReactions: ownReactions, ); state?.addMessage(newMessage); @@ -1075,73 +1082,24 @@ class ChannelClientState { void _listenReactionDeleted() { _subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) { - final reaction = event.reaction; - final message = event.message; - _removeMessageReaction(message, reaction); - })); - } - - void _removeMessageReaction(Message message, Reaction reaction) { - if (message.parentId == null || message.showInChannel == true) { - _channelState = _channelState.copyWith( - messages: _channelState?.messages?.map((m) { - if (m.id == message.id) { - return _removeReactionFromMessage(m, reaction); - } - return m; - })?.toList(), + final userId = _channel.client.state.user.id; + final message = event.message.copyWith( + ownReactions: [...event.message.latestReactions] + ..removeWhere((it) => it.userId != userId), ); - } - - if (message.parentId != null) { - final newThreads = threads; - if (newThreads.containsKey(message.parentId)) { - newThreads[message.parentId] = newThreads[message.parentId].map((m) { - if (m.id == message.id) { - return _removeReactionFromMessage(m, reaction); - } - return m; - }).toList(); - _threads = newThreads; - } - } + addMessage(message); + })); } void _listenReactions() { - _subscriptions.add(_channel - .on( - EventType.reactionNew, - ) - .listen((event) { - final message = event.message; - _addMessageReaction(message, event.reaction); - })); - } - - void _addMessageReaction(Message message, Reaction reaction) { - if (message.parentId == null || message.showInChannel == true) { - _channelState = _channelState.copyWith( - messages: _channelState.messages.map((m) { - if (message.id == m.id) { - return _addReactionToMessage(m, reaction); - } - return m; - }).toList(), + _subscriptions.add(_channel.on(EventType.reactionNew).listen((event) { + final userId = _channel.client.state.user.id; + final message = event.message.copyWith( + ownReactions: [...event.message.latestReactions] + ..removeWhere((it) => it.userId != userId), ); - } - - if (message.parentId != null) { - final newThreads = threads; - if (newThreads.containsKey(message.parentId)) { - newThreads[message.parentId] = newThreads[message.parentId].map((m) { - if (message.id == m.id) { - return _addReactionToMessage(m, reaction); - } - return m; - }).toList(); - _threads = newThreads; - } - } + addMessage(message); + })); } void _listenMessageUpdated() { @@ -1151,13 +1109,12 @@ class ChannelClientState { EventType.reactionUpdated, ) .listen((event) { - final message = event.message; - addMessage(message.copyWith( - ownReactions: message.latestReactions - .where( - (element) => element.user?.id == _channel._client.state.user.id) - .toList(), - )); + final userId = _channel.client.state.user.id; + final message = event.message.copyWith( + ownReactions: [...event.message.latestReactions] + ..removeWhere((it) => it.userId != userId), + ); + addMessage(message); })); } @@ -1240,66 +1197,6 @@ class ChannelClientState { })); } - Message _addReactionToMessage(Message message, Reaction reaction) { - final newMessage = message.copyWith( - latestReactions: message.latestReactions..add(reaction), - reactionCounts: { - ...message.reactionCounts ?? {}, - reaction.type: (message.reactionCounts == null - ? 0 - : message.reactionCounts[reaction.type] ?? 0) + - 1, - }, - reactionScores: { - ...message.reactionScores ?? {}, - reaction.type: (message.reactionScores == null - ? 0 - : message.reactionScores[reaction.type] ?? 0) + - reaction.score, - }, - ); - - if (reaction.user.id == _channel.client.state.user.id) { - return newMessage.copyWith( - ownReactions: message.ownReactions..add(reaction), - ); - } - - return newMessage; - } - - Message _removeReactionFromMessage(Message message, Reaction reaction) { - final newMessage = message.copyWith( - latestReactions: message.latestReactions - ..removeWhere( - (r) => r.type == reaction.type && r.userId == reaction.userId), - reactionCounts: { - ...message.reactionCounts, - reaction.type: (message.reactionCounts[reaction.type] ?? 0) - 1, - }, - reactionScores: { - ...message.reactionScores ?? {}, - reaction.type: max( - (message.reactionScores == null - ? 0 - : message.reactionScores[reaction.type] ?? 0) - - reaction.score, - 0), - }, - ); - - newMessage.reactionCounts.removeWhere((_, v) => v <= 0); - - if (reaction.user.id == _channel.client.state.user.id) { - return newMessage.copyWith( - ownReactions: message.ownReactions - ..removeWhere((r) => r.type == reaction.type), - ); - } - - return newMessage; - } - /// Channel message list List get messages => _channelState.messages; From 5347ba78d0854f22c9e1c50348eee729ee41d479 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 3 Feb 2021 13:33:33 +0530 Subject: [PATCH 6/7] [LLC] Fix reaction tests Signed-off-by: Sahil Kumar --- packages/stream_chat/test/src/api/channel_test.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 9baf4235..32ac65c2 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -7,6 +7,7 @@ import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/models/event.dart'; import 'package:stream_chat/src/models/message.dart'; import 'package:stream_chat/src/models/reaction.dart'; +import 'package:stream_chat/src/models/own_user.dart'; import 'package:test/test.dart'; class MockDio extends Mock implements DioForNative {} @@ -379,7 +380,8 @@ void main() { 'api-key', httpClient: mockDio, tokenProvider: (_) async => '', - ); + )..state.user = OwnUser(id: 'test-id'); + final channelClient = client.channel('messaging', id: 'testid'); final reactionType = 'test'; @@ -422,7 +424,8 @@ void main() { 'api-key', httpClient: mockDio, tokenProvider: (_) async => '', - ); + )..state.user = OwnUser(id: 'test-id'); + final channelClient = client.channel('messaging', id: 'testid'); when(mockDio.delete('/messages/messageid/reaction/test')) From d0ff54cbf382179db0ac6365dca304201db2822c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 3 Feb 2021 13:48:13 +0530 Subject: [PATCH 7/7] [LLC] save one extra rebuild Signed-off-by: Sahil Kumar --- packages/stream_chat/lib/src/api/channel.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index 39ed445e..7359c343 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -328,7 +328,6 @@ class Channel { ); final reactionResp = _client.decode(res.data, SendReactionResponse.fromJson); - state?.addMessage(reactionResp.message); return reactionResp; } catch (_) { // Reset the message if the update fails