diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/ChannelApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/ChannelApi.kt index 332dcb6c608..de531726c7d 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/ChannelApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/ChannelApi.kt @@ -22,11 +22,11 @@ import io.getstream.chat.android.client.api2.UrlQueryPayload import io.getstream.chat.android.client.api2.model.requests.PinnedMessagesRequest import io.getstream.chat.android.client.api2.model.response.ChannelResponse import io.getstream.chat.android.client.api2.model.response.EventResponse -import io.getstream.chat.android.client.api2.model.response.MessagesResponse import io.getstream.chat.android.client.api2.model.response.QueryChannelsResponse import io.getstream.chat.android.client.api2.model.response.QueryGroupedChannelsResponse import io.getstream.chat.android.client.call.RetrofitCall import io.getstream.chat.android.network.models.ChannelGetOrCreateRequest +import io.getstream.chat.android.network.models.GetPinnedMessagesResponse import io.getstream.chat.android.network.models.GroupedQueryChannelsRequest import io.getstream.chat.android.network.models.HideChannelRequest import io.getstream.chat.android.network.models.MarkDeliveredRequest @@ -179,7 +179,7 @@ internal interface ChannelApi { @Path("type") channelType: String, @Path("id") channelId: String, @UrlQueryPayload @Query("payload") payload: PinnedMessagesRequest, - ): RetrofitCall + ): RetrofitCall @POST("/channels/delivered") fun markDelivered( 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 4d1c5a2a5a3..4b9ed9223a5 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 @@ -18,11 +18,10 @@ package io.getstream.chat.android.client.api2.endpoint import io.getstream.chat.android.client.api.AuthenticatedApi 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.ReactionResponse import io.getstream.chat.android.client.call.RetrofitCall import io.getstream.chat.android.network.models.CreateDraftResponse import io.getstream.chat.android.network.models.GetReactionsResponse +import io.getstream.chat.android.network.models.GetRepliesResponse import io.getstream.chat.android.network.models.MessageActionRequest import io.getstream.chat.android.network.models.QueryDraftsRequest import io.getstream.chat.android.network.models.QueryDraftsResponse @@ -31,6 +30,7 @@ import io.getstream.chat.android.network.models.QueryReactionsResponse import io.getstream.chat.android.network.models.Response import io.getstream.chat.android.network.models.SendMessageRequest import io.getstream.chat.android.network.models.SendReactionRequest +import io.getstream.chat.android.network.models.SendReactionResponse import io.getstream.chat.android.network.models.TranslateMessageRequest import io.getstream.chat.android.network.models.UpdateMessagePartialRequest import io.getstream.chat.android.network.models.UpdateMessageRequest @@ -114,7 +114,7 @@ internal interface MessageApi { fun sendReaction( @Path("id") messageId: String, @Body request: SendReactionRequest, - ): RetrofitCall + ): RetrofitCall @DELETE("/messages/{id}/reaction/{type}") fun deleteReaction( @@ -145,26 +145,26 @@ internal interface MessageApi { fun getReplies( @Path("parent_id") messageId: String, @Query("limit") limit: Int, - ): RetrofitCall + ): RetrofitCall @GET("/messages/{parent_id}/replies?sort=[{\"field\":\"created_at\",\"direction\":1}]") fun getNewerReplies( @Path("parent_id") parentId: String, @Query("limit") limit: Int, @Query("id_gt") lastId: String?, - ): RetrofitCall + ): RetrofitCall @GET("/messages/{parent_id}/replies") fun getRepliesMore( @Path("parent_id") messageId: String, @Query("limit") limit: Int, @Query("id_lt") firstId: String, - ): RetrofitCall + ): RetrofitCall @GET("/messages/{parent_id}/replies") fun getRepliesAround( @Path("parent_id") parentId: String, @Query("limit") limit: Int, @Query("id_around") aroundId: String, - ): RetrofitCall + ): RetrofitCall } diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/MessagesResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/GetPinnedMessagesResponse.kt similarity index 55% rename from stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/MessagesResponse.kt rename to stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/GetPinnedMessagesResponse.kt index f045b43dabe..78d7575b12c 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/MessagesResponse.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/GetPinnedMessagesResponse.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.network.models.MessageResponse +/** + * + */ +@com.squareup.moshi.JsonClass(generateAdapter = true) +internal data class GetPinnedMessagesResponse( + @Json(name = "duration") + internal val duration: String, -@JsonClass(generateAdapter = true) -internal data class MessagesResponse( - val messages: List, + @Json(name = "messages") + internal val messages: List = emptyList(), ) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/ReactionResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/GetRepliesResponse.kt similarity index 54% rename from stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/ReactionResponse.kt rename to stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/GetRepliesResponse.kt index 70564a02e09..0a343b6df47 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/ReactionResponse.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/GetRepliesResponse.kt @@ -14,11 +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.JsonClass +import com.squareup.moshi.Json + +/** + * Basic response information + */ +@com.squareup.moshi.JsonClass(generateAdapter = true) +internal data class GetRepliesResponse( + @Json(name = "duration") + internal val duration: String, -@JsonClass(generateAdapter = true) -internal data class ReactionResponse( - val reaction: io.getstream.chat.android.network.models.ReactionResponse, + @Json(name = "messages") + internal val messages: List = emptyList(), ) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/SendReactionResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/SendReactionResponse.kt new file mode 100644 index 00000000000..675dad03dd6 --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/SendReactionResponse.kt @@ -0,0 +1,41 @@ +/* + * 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 + +/** + * Basic response information + */ +@com.squareup.moshi.JsonClass(generateAdapter = true) +internal data class SendReactionResponse( + @Json(name = "duration") + internal val duration: String, + + @Json(name = "message") + internal val message: io.getstream.chat.android.network.models.MessageResponse, + + @Json(name = "reaction") + internal val reaction: io.getstream.chat.android.network.models.ReactionResponse, +) 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 4432af9b41c..0de8380dda5 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 @@ -49,12 +49,10 @@ import io.getstream.chat.android.client.api2.model.response.ChannelResponse import io.getstream.chat.android.client.api2.model.response.EventResponse import io.getstream.chat.android.client.api2.model.response.FlagResponse 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.MuteUserResponse import io.getstream.chat.android.client.api2.model.response.QueryChannelsResponse 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.ReactionResponse import io.getstream.chat.android.client.api2.model.response.SyncHistoryResponse import io.getstream.chat.android.client.call.RetrofitCall import io.getstream.chat.android.client.parser.toMap @@ -121,7 +119,9 @@ 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.GetPinnedMessagesResponse import io.getstream.chat.android.network.models.GetReactionsResponse +import io.getstream.chat.android.network.models.GetRepliesResponse import io.getstream.chat.android.network.models.GetThreadResponse import io.getstream.chat.android.network.models.GetUserGroupResponse import io.getstream.chat.android.network.models.GroupedChannelsGroupRequest @@ -168,6 +168,7 @@ import io.getstream.chat.android.network.models.SearchResponse import io.getstream.chat.android.network.models.SearchRolesResponse import io.getstream.chat.android.network.models.SearchUserGroupsResponse import io.getstream.chat.android.network.models.SendEventRequest +import io.getstream.chat.android.network.models.SendReactionResponse import io.getstream.chat.android.network.models.SharedLocationResponse import io.getstream.chat.android.network.models.SharedLocationResponseData import io.getstream.chat.android.network.models.SortParamRequest @@ -491,7 +492,7 @@ internal class MoshiChatApiTest { @ParameterizedTest @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#sendReactionInput") - fun testSendReaction(call: RetrofitCall, expected: KClass<*>) = runTest { + fun testSendReaction(call: RetrofitCall, expected: KClass<*>) = runTest { // given val api = mock() whenever(api.sendReaction(any(), any())).doReturn(call) @@ -1302,7 +1303,7 @@ internal class MoshiChatApiTest { @ParameterizedTest @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#getPinnedMessagesInput") - fun testGetPinnedMessages(call: RetrofitCall, expected: KClass<*>) = runTest { + fun testGetPinnedMessages(call: RetrofitCall, expected: KClass<*>) = runTest { // given val api = mock() whenever(api.getPinnedMessages(any(), any(), any())).doReturn(call) @@ -1734,7 +1735,7 @@ internal class MoshiChatApiTest { @ParameterizedTest @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#getNewerRepliesInput") - fun testGetNewerReplies(call: RetrofitCall, expected: KClass<*>) = runTest { + fun testGetNewerReplies(call: RetrofitCall, expected: KClass<*>) = runTest { // given val api = mock() whenever(api.getNewerReplies(any(), any(), any())).doReturn(call) @@ -1753,7 +1754,7 @@ internal class MoshiChatApiTest { @ParameterizedTest @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#getRepliesInput") - fun testGetReplies(call: RetrofitCall, expected: KClass<*>) = runTest { + fun testGetReplies(call: RetrofitCall, expected: KClass<*>) = runTest { // given val api = mock() whenever(api.getReplies(any(), any())).doReturn(call) @@ -1771,7 +1772,7 @@ internal class MoshiChatApiTest { @ParameterizedTest @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#getRepliesMoreInput") - fun testGetRepliesMore(call: RetrofitCall, expected: KClass<*>) = runTest { + fun testGetRepliesMore(call: RetrofitCall, expected: KClass<*>) = runTest { // given val api = mock() whenever(api.getRepliesMore(any(), any(), any())).doReturn(call) @@ -1790,7 +1791,7 @@ internal class MoshiChatApiTest { @ParameterizedTest @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#getRepliesAroundInput") - fun testGetRepliesAround(call: RetrofitCall, expected: KClass<*>) = runTest { + fun testGetRepliesAround(call: RetrofitCall, expected: KClass<*>) = runTest { // given val api = mock() whenever(api.getRepliesAround(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 1a2ef9969a0..bc535859a84 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 @@ -32,12 +32,10 @@ import io.getstream.chat.android.client.api2.model.response.ChannelResponse import io.getstream.chat.android.client.api2.model.response.EventResponse import io.getstream.chat.android.client.api2.model.response.FlagResponse 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.MuteUserResponse import io.getstream.chat.android.client.api2.model.response.QueryChannelsResponse 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.ReactionResponse import io.getstream.chat.android.client.api2.model.response.SyncHistoryResponse import io.getstream.chat.android.client.utils.RetroError import io.getstream.chat.android.client.utils.RetroSuccess @@ -56,7 +54,9 @@ 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.GetPinnedMessagesResponse import io.getstream.chat.android.network.models.GetReactionsResponse +import io.getstream.chat.android.network.models.GetRepliesResponse import io.getstream.chat.android.network.models.GetThreadResponse import io.getstream.chat.android.network.models.GetUserGroupResponse import io.getstream.chat.android.network.models.ListDevicesResponse @@ -79,6 +79,7 @@ import io.getstream.chat.android.network.models.SearchResponse import io.getstream.chat.android.network.models.SearchResult import io.getstream.chat.android.network.models.SearchRolesResponse import io.getstream.chat.android.network.models.SearchUserGroupsResponse +import io.getstream.chat.android.network.models.SendReactionResponse import io.getstream.chat.android.network.models.SharedLocationResponse import io.getstream.chat.android.network.models.SortParamRequest import io.getstream.chat.android.network.models.UnblockUsersResponse @@ -225,10 +226,16 @@ internal object MoshiChatApiTestArguments { @JvmStatic fun sendReactionInput() = listOf( Arguments.of( - RetroSuccess(ReactionResponse(Mother.randomReactionResponse())).toRetrofitCall(), + RetroSuccess( + SendReactionResponse( + duration = "1ms", + message = Mother.randomMessageResponse(), + reaction = 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 @@ -333,7 +340,7 @@ internal object MoshiChatApiTestArguments { fun stopWatchingInput() = completableResponseArguments() @JvmStatic - fun getPinnedMessagesInput() = messagesResponseArguments() + fun getPinnedMessagesInput() = pinnedMessagesResponseArguments() @JvmStatic fun updateChannelInput() = channelResponseArguments() @@ -411,16 +418,16 @@ internal object MoshiChatApiTestArguments { ) @JvmStatic - fun getNewerRepliesInput() = messagesResponseArguments() + fun getNewerRepliesInput() = repliesResponseArguments() @JvmStatic - fun getRepliesInput() = messagesResponseArguments() + fun getRepliesInput() = repliesResponseArguments() @JvmStatic - fun getRepliesMoreInput() = messagesResponseArguments() + fun getRepliesMoreInput() = repliesResponseArguments() @JvmStatic - fun getRepliesAroundInput() = messagesResponseArguments() + fun getRepliesAroundInput() = repliesResponseArguments() @JvmStatic fun sendActionInput() = messageResponseArguments() @@ -931,12 +938,23 @@ internal object MoshiChatApiTestArguments { Arguments.of(RetroError(statusCode = 500).toRetrofitCall(), Result.Failure::class), ) - private fun messagesResponseArguments() = listOf( + private fun repliesResponseArguments() = listOf( + Arguments.of( + RetroSuccess(GetRepliesResponse(duration = "1ms", messages = listOf(Mother.randomMessageResponse()))) + .toRetrofitCall(), + Result.Success::class, + ), + Arguments.of(RetroError(statusCode = 500).toRetrofitCall(), Result.Failure::class), + ) + + private fun pinnedMessagesResponseArguments() = listOf( Arguments.of( - RetroSuccess(MessagesResponse(listOf(Mother.randomMessageResponse()))).toRetrofitCall(), + RetroSuccess( + GetPinnedMessagesResponse(duration = "1ms", messages = listOf(Mother.randomMessageResponse())), + ).toRetrofitCall(), Result.Success::class, ), - Arguments.of(RetroError(statusCode = 500).toRetrofitCall(), Result.Failure::class), + Arguments.of(RetroError(statusCode = 500).toRetrofitCall(), Result.Failure::class), ) private fun updateUsersResponseArguments(): List { diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/MessageListResponseParsingTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/MessageListResponseParsingTest.kt new file mode 100644 index 00000000000..03dd7263e2d --- /dev/null +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/MessageListResponseParsingTest.kt @@ -0,0 +1,101 @@ +/* + * 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 com.squareup.moshi.JsonDataException +import io.getstream.chat.android.network.models.GetPinnedMessagesResponse +import io.getstream.chat.android.network.models.GetRepliesResponse +import io.getstream.chat.android.network.models.SendReactionResponse +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +/** The replies, pinned messages and send reaction responses parsed through their generated models. */ +internal class MessageListResponseParsingTest { + + private val parser = ParserFactory.createMoshiChatParser() + + @Test + fun `Replies parse their messages`() { + val response = parser.fromJson("""{ "duration": "1ms", "messages": [$MESSAGE] }""", GetRepliesResponse::class.java) + + response.messages.map { it.id } shouldBeEqualTo listOf("message-id") + } + + @Test + fun `Pinned messages parse their messages`() { + val response = parser.fromJson( + """{ "duration": "1ms", "messages": [$MESSAGE] }""", + GetPinnedMessagesResponse::class.java, + ) + + response.messages.map { it.id } shouldBeEqualTo listOf("message-id") + } + + @Test + fun `A send reaction response parses its reaction`() { + val response = parser.fromJson( + """{ "duration": "1ms", "message": $MESSAGE, "reaction": $REACTION }""", + SendReactionResponse::class.java, + ) + + response.reaction.type shouldBeEqualTo "like" + response.reaction.messageId shouldBeEqualTo "message-id" + } + + @Test + fun `A response without a duration is rejected`() { + assertThrows { + parser.fromJson("""{ "messages": [$MESSAGE] }""", GetRepliesResponse::class.java) + } + assertThrows { + parser.fromJson("""{ "messages": [$MESSAGE] }""", GetPinnedMessagesResponse::class.java) + } + } + + @Test + fun `A send reaction response without its message is rejected`() { + assertThrows { + parser.fromJson("""{ "duration": "1ms", "reaction": $REACTION }""", SendReactionResponse::class.java) + } + } + + private companion object { + private const val USER = """ + { + "id": "jaewoong", "role": "user", "language": "en", "banned": false, "online": false, + "created_at": "2026-09-22T10:00:00.000Z", "updated_at": "2026-09-22T10:00:00.000Z" + } + """ + + private const val MESSAGE = """ + { + "id": "message-id", "cid": "messaging:general", "text": "hello", "html": "

hello

", + "type": "regular", "created_at": "2026-09-22T10:00:00.000Z", "updated_at": "2026-09-22T10:00:00.000Z", + "mentioned_channel": false, "mentioned_here": false, "pinned": false, "shadowed": false, + "silent": false, "reply_count": 0, "deleted_reply_count": 0, "user": $USER + } + """ + + private const val REACTION = """ + { + "type": "like", "message_id": "message-id", "user_id": "jaewoong", "score": 1, + "created_at": "2026-09-22T10:00:00.000Z", "updated_at": "2026-09-22T10:00:00.000Z", "user": $USER + } + """ + } +}