diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/MessageApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/MessageApi.kt index e91d5f6f8f8b..f8a99f97a643 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/MessageApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/MessageApi.kt @@ -24,13 +24,13 @@ import io.getstream.chat.android.client.api2.model.response.DraftMessageResponse import io.getstream.chat.android.client.api2.model.response.MessageResponse import io.getstream.chat.android.client.api2.model.response.MessagesResponse import io.getstream.chat.android.client.api2.model.response.QueryDraftMessagesResponse -import io.getstream.chat.android.client.api2.model.response.QueryReactionsResponse import io.getstream.chat.android.client.api2.model.response.ReactionResponse -import io.getstream.chat.android.client.api2.model.response.ReactionsResponse import io.getstream.chat.android.client.call.RetrofitCall +import io.getstream.chat.android.network.models.GetReactionsResponse import io.getstream.chat.android.network.models.MessageActionRequest import io.getstream.chat.android.network.models.QueryDraftsRequest import io.getstream.chat.android.network.models.QueryReactionsRequest +import io.getstream.chat.android.network.models.QueryReactionsResponse import io.getstream.chat.android.network.models.Response import io.getstream.chat.android.network.models.SendReactionRequest import io.getstream.chat.android.network.models.TranslateMessageRequest @@ -133,7 +133,7 @@ internal interface MessageApi { @Path("id") messageId: String, @Query("offset") offset: Int, @Query("limit") limit: Int, - ): RetrofitCall + ): RetrofitCall @POST("/messages/{id}/reactions") fun queryReactions( diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DomainMapping.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DomainMapping.kt index 5fd75c4c8017..fb2c6458fe86 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DomainMapping.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DomainMapping.kt @@ -145,6 +145,7 @@ import io.getstream.chat.android.network.models.PollVotesResponse import io.getstream.chat.android.network.models.PrivacySettingsResponse import io.getstream.chat.android.network.models.PushPreferencesResponse import io.getstream.chat.android.network.models.QueryPollsResponse +import io.getstream.chat.android.network.models.ReactionResponse import io.getstream.chat.android.network.models.UnreadCountsChannel import io.getstream.chat.android.network.models.UnreadCountsChannelType import io.getstream.chat.android.network.models.UnreadCountsThread @@ -160,6 +161,9 @@ import io.getstream.chat.android.network.models.Role as RoleDto import io.getstream.chat.android.network.models.ThreadParticipant as ThreadParticipantDto import io.getstream.chat.android.network.models.UserGroupMember as UserGroupMemberDto +/** `emoji_code` is sent as custom data rather than a declared field, in both directions. */ +internal const val EMOJI_CODE_KEY = "emoji_code" + @Suppress("TooManyFunctions", "LargeClass") internal class DomainMapping( val currentUserIdProvider: () -> UserId?, @@ -490,6 +494,27 @@ internal class DomainMapping( expires = expires, ) + /** + * Transforms [ReactionResponse] into [Reaction]. + * + * `emoji_code` is custom data on the backend rather than a declared field, so it arrives inside + * [ReactionResponse.custom] and is promoted here, staying out of [Reaction.extraData]. + */ + internal fun ReactionResponse.toDomain(): Reaction = Reaction( + createdAt = createdAt, + messageId = messageId, + score = score, + type = type, + updatedAt = updatedAt, + user = user.toDomain(), + userId = userId, + emojiCode = custom[EMOJI_CODE_KEY] as? String, + extraData = custom + .filterKeys { it != EMOJI_CODE_KEY } + .mapNotNull { (key, value) -> value?.let { key to it } } + .toMap(), + ) + /** * Transforms [DownstreamReactionDto] to [Reaction]. */ diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DtoMapping.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DtoMapping.kt index 86ad742391af..b8fca3a72988 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DtoMapping.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DtoMapping.kt @@ -292,7 +292,7 @@ internal class DtoMapping( createdAt = createdAt, score = score, updatedAt = updatedAt, - custom = if (emojiCode != null) extraData + ("emoji_code" to emojiCode) else extraData, + custom = if (emojiCode != null) extraData + (EMOJI_CODE_KEY to emojiCode) else extraData, ) /** diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/MoshiChatParser.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/MoshiChatParser.kt index fa2ee625c308..296bc2a9a4df 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/MoshiChatParser.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/MoshiChatParser.kt @@ -60,6 +60,7 @@ import io.getstream.chat.android.client.parser2.adapters.PollOptionInputAdapter import io.getstream.chat.android.client.parser2.adapters.PollOptionRequestAdapter import io.getstream.chat.android.client.parser2.adapters.PollOptionResponseDataAdapter import io.getstream.chat.android.client.parser2.adapters.PollResponseDataAdapter +import io.getstream.chat.android.client.parser2.adapters.ReactionResponseAdapter import io.getstream.chat.android.client.parser2.adapters.UpdatePollOptionRequestAdapter import io.getstream.chat.android.client.parser2.adapters.UpdatePollRequestAdapter import io.getstream.chat.android.client.parser2.adapters.UpstreamMemberDataDtoAdapter @@ -131,6 +132,7 @@ internal class MoshiChatParser( .add(GetOGResponseAdapter) .add(PollOptionResponseDataAdapter) .add(PollResponseDataAdapter) + .add(ReactionResponseAdapter) .add( CreatePollRequest.VotingVisibility::class.java, CreatePollRequest.VotingVisibility.VotingVisibilityAdapter(), diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/ReactionResponseAdapter.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/ReactionResponseAdapter.kt new file mode 100644 index 000000000000..5f32b4c4db1f --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/ReactionResponseAdapter.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.client.parser2.adapters + +import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter +import com.squareup.moshi.ToJson +import io.getstream.chat.android.network.models.ReactionResponse + +/** + * Collects the root-level custom fields of a reaction into `custom`, which is how the API sends + * them. + */ +internal object ReactionResponseAdapter : + CustomObjectDtoAdapter( + ReactionResponse::class, + extraDataPropertyName = "custom", + ) { + + @FromJson + fun fromJson( + jsonReader: JsonReader, + mapAdapter: JsonAdapter>, + valueAdapter: JsonAdapter, + ): ReactionResponse? = parseWithExtraData(jsonReader, mapAdapter, valueAdapter) + + @ToJson + @Suppress("UNUSED_PARAMETER") + fun toJson(jsonWriter: JsonWriter, value: ReactionResponse): Unit = + error("Can't convert this to Json") +} diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/ReactionsResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/GetReactionsResponse.kt similarity index 57% rename from stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/ReactionsResponse.kt rename to stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/GetReactionsResponse.kt index 8d38c810b27f..06b876ef51ea 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/ReactionsResponse.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/GetReactionsResponse.kt @@ -14,12 +14,25 @@ * limitations under the License. */ -package io.getstream.chat.android.client.api2.model.response +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport", +) + +package io.getstream.chat.android.network.models + +import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import io.getstream.chat.android.client.api2.model.dto.DownstreamReactionDto +/** + * + */ +@com.squareup.moshi.JsonClass(generateAdapter = true) +internal data class GetReactionsResponse( + @Json(name = "duration") + internal val duration: String, -@JsonClass(generateAdapter = true) -internal data class ReactionsResponse( - val reactions: List, + @Json(name = "reactions") + internal val reactions: List = emptyList(), ) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/QueryReactionsResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/QueryReactionsResponse.kt similarity index 54% rename from stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/QueryReactionsResponse.kt rename to stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/QueryReactionsResponse.kt index d824a57c99f0..e9bbab4a45d7 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/QueryReactionsResponse.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/QueryReactionsResponse.kt @@ -14,19 +14,31 @@ * limitations under the License. */ -package io.getstream.chat.android.client.api2.model.response +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport", +) + +package io.getstream.chat.android.network.models -import com.squareup.moshi.JsonClass -import io.getstream.chat.android.client.api2.model.dto.DownstreamReactionDto +import com.squareup.moshi.Json /** - * Response for querying reactions on a message. - * - * @property reactions The list of reactions returned by the query. - * @property next The identifier for the next page of reactions. + * Basic response information */ -@JsonClass(generateAdapter = true) +@com.squareup.moshi.JsonClass(generateAdapter = true) internal data class QueryReactionsResponse( - val reactions: List, - val next: String?, + @Json(name = "duration") + internal val duration: String, + + @Json(name = "reactions") + internal val reactions: List = emptyList(), + + @Json(name = "next") + internal val next: String? = null, + + @Json(name = "prev") + internal val prev: String? = null, ) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/ReactionResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/ReactionResponse.kt new file mode 100644 index 000000000000..5cb7ee12c5fc --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/ReactionResponse.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport", +) + +package io.getstream.chat.android.network.models + +import com.squareup.moshi.Json + +/** + * + */ +@com.squareup.moshi.JsonClass(generateAdapter = true) +internal data class ReactionResponse( + @Json(name = "created_at") + internal val createdAt: java.util.Date, + + @Json(name = "message_id") + internal val messageId: String, + + @Json(name = "score") + internal val score: Int, + + @Json(name = "type") + internal val type: String, + + @Json(name = "updated_at") + internal val updatedAt: java.util.Date, + + @Json(name = "user_id") + internal val userId: String, + + @Json(name = "custom") + internal val custom: Map = emptyMap(), + + @Json(name = "user") + internal val user: UserResponse, +) diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/Mother.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/Mother.kt index 0528b4868f2b..717d9d8a580d 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/Mother.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/Mother.kt @@ -104,6 +104,7 @@ import io.getstream.chat.android.network.models.PollResponseData import io.getstream.chat.android.network.models.PollVoteResponseData import io.getstream.chat.android.network.models.PollVotesResponse import io.getstream.chat.android.network.models.QueryPollsResponse +import io.getstream.chat.android.network.models.ReactionResponse import io.getstream.chat.android.network.models.ThreadParticipant import io.getstream.chat.android.network.models.UnblockUsersResponse import io.getstream.chat.android.network.models.UnreadCountsChannel @@ -1386,6 +1387,22 @@ internal object Mother { next = next, ) + fun randomReactionResponse( + messageId: String = randomString(), + type: String = randomString(), + userId: String = randomString(), + custom: Map = emptyMap(), + ): ReactionResponse = ReactionResponse( + messageId = messageId, + type = type, + score = randomInt(), + createdAt = randomDate(), + updatedAt = randomDate(), + userId = userId, + user = randomUserResponse(id = userId), + custom = custom, + ) + fun randomPollResponseData( id: String = randomString(), name: String = randomString(), diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTest.kt index d22f4771625e..62bec2e2d898 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTest.kt @@ -57,11 +57,9 @@ import io.getstream.chat.android.client.api2.model.response.QueryChannelsRespons import io.getstream.chat.android.client.api2.model.response.QueryDraftMessagesResponse import io.getstream.chat.android.client.api2.model.response.QueryGroupedChannelsGroup import io.getstream.chat.android.client.api2.model.response.QueryGroupedChannelsResponse -import io.getstream.chat.android.client.api2.model.response.QueryReactionsResponse import io.getstream.chat.android.client.api2.model.response.QueryRemindersResponse import io.getstream.chat.android.client.api2.model.response.QueryThreadsResponse import io.getstream.chat.android.client.api2.model.response.ReactionResponse -import io.getstream.chat.android.client.api2.model.response.ReactionsResponse import io.getstream.chat.android.client.api2.model.response.ReminderResponse import io.getstream.chat.android.client.api2.model.response.SearchMessagesResponse import io.getstream.chat.android.client.api2.model.response.SyncHistoryResponse @@ -128,6 +126,7 @@ import io.getstream.chat.android.network.models.EventRequest import io.getstream.chat.android.network.models.GetApplicationResponse import io.getstream.chat.android.network.models.GetBlockedUsersResponse import io.getstream.chat.android.network.models.GetOGResponse +import io.getstream.chat.android.network.models.GetReactionsResponse import io.getstream.chat.android.network.models.GetUserGroupResponse import io.getstream.chat.android.network.models.GroupedChannelsGroupRequest import io.getstream.chat.android.network.models.GroupedQueryChannelsRequest @@ -156,6 +155,7 @@ import io.getstream.chat.android.network.models.QueryPollVotesRequest import io.getstream.chat.android.network.models.QueryPollsRequest import io.getstream.chat.android.network.models.QueryPollsResponse import io.getstream.chat.android.network.models.QueryReactionsRequest +import io.getstream.chat.android.network.models.QueryReactionsResponse import io.getstream.chat.android.network.models.QueryRemindersRequest import io.getstream.chat.android.network.models.QueryUsersResponse import io.getstream.chat.android.network.models.RemoveUserGroupMembersRequest @@ -455,7 +455,7 @@ internal class MoshiChatApiTest { @ParameterizedTest @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#getReactionsInput") - fun testGetReactions(call: RetrofitCall, expected: KClass<*>) = runTest { + fun testGetReactions(call: RetrofitCall, expected: KClass<*>) = runTest { // given val api = mock() whenever(api.getReactions(any(), any(), any())).doReturn(call) diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTestArguments.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTestArguments.kt index 4b6472803958..4a6621d7bbd4 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTestArguments.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTestArguments.kt @@ -40,10 +40,8 @@ import io.getstream.chat.android.client.api2.model.response.QueryChannelsRespons import io.getstream.chat.android.client.api2.model.response.QueryDraftMessagesResponse import io.getstream.chat.android.client.api2.model.response.QueryGroupedChannelsGroup import io.getstream.chat.android.client.api2.model.response.QueryGroupedChannelsResponse -import io.getstream.chat.android.client.api2.model.response.QueryReactionsResponse import io.getstream.chat.android.client.api2.model.response.QueryThreadsResponse import io.getstream.chat.android.client.api2.model.response.ReactionResponse -import io.getstream.chat.android.client.api2.model.response.ReactionsResponse import io.getstream.chat.android.client.api2.model.response.ReminderResponse import io.getstream.chat.android.client.api2.model.response.SearchMessagesResponse import io.getstream.chat.android.client.api2.model.response.SyncHistoryResponse @@ -65,6 +63,7 @@ import io.getstream.chat.android.network.models.CreateUserGroupResponse import io.getstream.chat.android.network.models.GetApplicationResponse import io.getstream.chat.android.network.models.GetBlockedUsersResponse import io.getstream.chat.android.network.models.GetOGResponse +import io.getstream.chat.android.network.models.GetReactionsResponse import io.getstream.chat.android.network.models.GetUserGroupResponse import io.getstream.chat.android.network.models.ListDevicesResponse import io.getstream.chat.android.network.models.ListUserGroupsResponse @@ -75,6 +74,7 @@ import io.getstream.chat.android.network.models.PollVoteResponse import io.getstream.chat.android.network.models.PollVotesResponse import io.getstream.chat.android.network.models.QueryBannedUsersResponse import io.getstream.chat.android.network.models.QueryPollsResponse +import io.getstream.chat.android.network.models.QueryReactionsResponse import io.getstream.chat.android.network.models.QueryUsersResponse import io.getstream.chat.android.network.models.RemoveUserGroupMembersResponse import io.getstream.chat.android.network.models.Response @@ -189,10 +189,15 @@ internal object MoshiChatApiTestArguments { @JvmStatic fun getReactionsInput() = listOf( Arguments.of( - RetroSuccess(ReactionsResponse(listOf(Mother.randomDownstreamReactionDto()))).toRetrofitCall(), + RetroSuccess( + GetReactionsResponse( + duration = randomString(), + reactions = listOf(Mother.randomReactionResponse()), + ), + ).toRetrofitCall(), Result.Success::class, ), - Arguments.of(RetroError(statusCode = 500).toRetrofitCall(), Result.Failure::class), + Arguments.of(RetroError(statusCode = 500).toRetrofitCall(), Result.Failure::class), ) @JvmStatic @@ -200,7 +205,8 @@ internal object MoshiChatApiTestArguments { Arguments.of( RetroSuccess( QueryReactionsResponse( - reactions = listOf(Mother.randomDownstreamReactionDto()), + duration = randomString(), + reactions = listOf(Mother.randomReactionResponse()), next = randomString(), ), ).toRetrofitCall(), diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DomainMappingTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DomainMappingTest.kt index 2700348922a4..555953fdb10a 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DomainMappingTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DomainMappingTest.kt @@ -60,6 +60,7 @@ import io.getstream.chat.android.client.Mother.randomPollVotesResponse import io.getstream.chat.android.client.Mother.randomPrivacySettingsDto import io.getstream.chat.android.client.Mother.randomQueryPollsResponse import io.getstream.chat.android.client.Mother.randomQueryRemindersResponse +import io.getstream.chat.android.client.Mother.randomReactionResponse import io.getstream.chat.android.client.Mother.randomRoleDto import io.getstream.chat.android.client.Mother.randomSearchWarningDto import io.getstream.chat.android.client.Mother.randomThreadParticipantDto @@ -1703,4 +1704,53 @@ internal class DomainMappingTest { ) } } + + @Test + fun `ReactionResponse is correctly mapped to Reaction`() { + val response = randomReactionResponse(custom = mapOf("weight" to 3)) + val sut = Fixture().get() + + val reaction = with(sut) { response.toDomain() } + + val expected = Reaction( + messageId = response.messageId, + type = response.type, + score = response.score, + user = with(sut) { response.user.toDomain() }, + userId = response.userId, + createdAt = response.createdAt, + updatedAt = response.updatedAt, + extraData = mapOf("weight" to 3), + deletedAt = null, + emojiCode = null, + ) + assertEquals(expected, reaction) + } + + @Test + fun `ReactionResponse takes userId from the top level field rather than the nested user`() { + // The fixture deliberately disagrees with itself: while the two ids match, as they do on the + // wire, either source satisfies the assertion and the mapper could read the wrong one unnoticed. + val response = randomReactionResponse(userId = "reaction-user") + .copy(user = randomUserResponse(id = "nested-user")) + val sut = Fixture().get() + + val reaction = with(sut) { response.toDomain() } + + assertEquals("reaction-user", reaction.userId) + assertEquals("nested-user", reaction.user?.id) + } + + @Test + fun `ReactionResponse maps emoji_code out of custom and keeps it out of extraData`() { + val dto = randomReactionResponse( + custom = mapOf("emoji_code" to "\uD83D\uDE04", "weight" to 3), + ) + val sut = Fixture().get() + + val reaction = with(sut) { dto.toDomain() } + + assertEquals("\uD83D\uDE04", reaction.emojiCode) + assertEquals(mapOf("weight" to 3), reaction.extraData) + } } diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/ReactionResponseParsingTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/ReactionResponseParsingTest.kt new file mode 100644 index 000000000000..7760e1c4d514 --- /dev/null +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/ReactionResponseParsingTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.client.parser2 + +import io.getstream.chat.android.network.models.GetReactionsResponse +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldHaveSize +import org.junit.jupiter.api.Test + +internal class ReactionResponseParsingTest { + private val parser = ParserFactory.createMoshiChatParser() + + @Test + fun `Deserialize a reactions response`() { + val reactions = parser.fromJson(REACTIONS_JSON, GetReactionsResponse::class.java).reactions + + reactions shouldHaveSize 2 + reactions.first().type shouldBeEqualTo "smile" + reactions.first().score shouldBeEqualTo 1 + reactions.first().messageId shouldBeEqualTo "messageId" + reactions.first().user.id shouldBeEqualTo "leandro" + } + + @Test + fun `Collect emoji_code and other root-level custom fields into custom`() { + val reactions = parser.fromJson(REACTIONS_JSON, GetReactionsResponse::class.java).reactions + + // The backend sends emoji_code as custom data rather than a declared field. + reactions.first().custom shouldBeEqualTo mapOf("emoji_code" to "😄", "weight" to 3.0) + reactions.last().custom shouldBeEqualTo emptyMap() + } + + companion object { + private const val REACTIONS_JSON = + """{ + "duration": "9ms", + "reactions": [ + { + "message_id": "messageId", + "user_id": "leandro", + "type": "smile", + "score": 1, + "created_at": "2026-06-29T08:36:59.000Z", + "updated_at": "2026-06-29T08:36:59.000Z", + "emoji_code": "😄", + "weight": 3, + "user": { + "id": "leandro", + "role": "user", + "language": "pt", + "banned": false, + "online": true, + "created_at": "2021-07-20T14:17:07.000Z", + "updated_at": "2026-07-31T11:38:42.000Z" + } + }, + { + "message_id": "messageId", + "user_id": "filip", + "type": "sad", + "score": 1, + "created_at": "2026-06-29T08:36:52.000Z", + "updated_at": "2026-06-29T08:36:52.000Z", + "user": { + "id": "filip", + "role": "user", + "language": "", + "banned": false, + "online": false, + "created_at": "2021-10-21T21:58:10.000Z", + "updated_at": "2026-08-05T03:32:10.000Z" + } + } + ] + }""" + } +}