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, + ); + } }