diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index 6117b5d7..7359c343 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -274,28 +274,111 @@ class Channel { bool enforceUnique = false, }) async { final messageId = message.id; + final now = DateTime.now(); + final user = _client.state.user; + + final latestReactions = [...message.latestReactions ?? []]; + if (enforceUnique) { + latestReactions.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); + final ownReactions = [...latestReactions] + ..removeWhere((it) => it.userId != user.id); + + 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); + return reactionResp; + } catch (_) { + // Reset the message if the update fails + state?.addMessage(message); + rethrow; + } } /// Delete a reaction from this channel - Future deleteReaction(Message message, Reaction reaction) { - _checkInitialized(); + Future deleteReaction( + Message message, Reaction reaction) async { + final type = reaction.type; + final user = _client.state.user; - return client - .delete('/messages/${message.id}/reaction/${reaction.type}') - .then((res) => _client.decode(res.data, EmptyResponse.fromJson)); + 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 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: latestReactions, + ownReactions: ownReactions, + ); + + state?.addMessage(newMessage); + + try { + final res = await client + .delete('/messages/${message.id}/reaction/${reaction.type}'); + return _client.decode(res.data, EmptyResponse.fromJson); + } catch (_) { + // Reset the message if the update fails + state?.addMessage(message); + rethrow; + } } /// Edit the channel custom data @@ -998,73 +1081,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() { @@ -1074,13 +1108,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); })); } @@ -1163,66 +1196,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; 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, + ); + } } diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 4b274a94..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'; @@ -396,6 +398,10 @@ void main() { await channelClient.sendReaction( Message( id: 'messageid', + reactionCounts: const {}, + reactionScores: const {}, + latestReactions: const [], + ownReactions: const [], ), reactionType, ); @@ -418,23 +424,21 @@ void main() { 'api-key', httpClient: mockDio, tokenProvider: (_) async => '', - ); - final channelClient = client.channel('messaging', id: 'testid'); + )..state.user = OwnUser(id: 'test-id'); - when(mockDio.post( - any, - data: anyNamed('data'), - )).thenAnswer((_) async => Response( - data: '{}', - statusCode: 200, - )); - await channelClient.watch(); + final channelClient = client.channel('messaging', id: 'testid'); 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'), ); 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 c970e92c..c44022bf 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -261,7 +261,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, ), 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)); }, ); }