From 308ebb27ad04ee8a6130b0ca3f757b8832920aed Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:54:58 +0200 Subject: [PATCH 1/2] Move message, reaction, reply, translate and action endpoints to v2 --- .../client/api2/endpoint/MessageApi.kt | 32 +-- .../android/client/api2/mapping/DtoMapping.kt | 24 ++- .../chat/android/client/di/ChatModule.kt | 2 +- .../client/api2/mapping/DtoMappingTest.kt | 29 +++ .../infrastructure/V2LaneBatchFiveTest.kt | 190 ++++++++++++++++++ .../chat/android/uitests/ui/BaseUiTest.kt | 2 +- 6 files changed, 258 insertions(+), 21 deletions(-) create mode 100644 stream-chat-android-client/src/test/java/io/getstream/chat/android/network/infrastructure/V2LaneBatchFiveTest.kt 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 19898bd3195f..90063150a4c8 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 @@ -52,20 +52,20 @@ internal interface MessageApi { /** * [REST documentation](https://getstream.io/chat/docs/rest/#messages-sendmessage) */ - @POST("/channels/{type}/{id}/message") + @POST("/api/v2/chat/channels/{type}/{id}/message") fun sendMessage( @Path("type") channelType: String, @Path("id") channelId: String, @Body message: SendMessageRequest, ): RetrofitCall - @GET("/messages/{id}") + @GET("/api/v2/chat/messages/{id}") fun getMessage(@Path("id") messageId: String): RetrofitCall /** - * [REST documentation]()https://getstream.io/chat/docs/rest/#messages-updatemessage) + * [REST documentation](https://getstream.io/chat/docs/rest/#messages-updatemessage) */ - @POST("/messages/{id}") + @POST("/api/v2/chat/messages/{id}") fun updateMessage( @Path("id") messageId: String, @Body message: UpdateMessageRequest, @@ -74,77 +74,77 @@ internal interface MessageApi { /** * [Rest documentation](https://getstream.io/chat/docs/rest/#messages-updatemessagepartial-request) */ - @PUT("/messages/{id}") + @PUT("/api/v2/chat/messages/{id}") fun partialUpdateMessage( @Path("id") messageId: String, @Body body: UpdateMessagePartialRequest, ): RetrofitCall - @DELETE("/messages/{id}") + @DELETE("/api/v2/chat/messages/{id}") fun deleteMessage( @Path("id") messageId: String, @Query("hard") hard: Boolean?, @Query("delete_for_me") deleteForMe: Boolean?, ): RetrofitCall - @POST("/messages/{id}/action") + @POST("/api/v2/chat/messages/{id}/action") fun sendAction( @Path("id") messageId: String, @Body request: MessageActionRequest, ): RetrofitCall - @POST("/messages/{id}/reaction") + @POST("/api/v2/chat/messages/{id}/reaction") fun sendReaction( @Path("id") messageId: String, @Body request: SendReactionRequest, ): RetrofitCall - @DELETE("/messages/{id}/reaction/{type}") + @DELETE("/api/v2/chat/messages/{id}/reaction/{type}") fun deleteReaction( @Path("id") messageId: String, @Path("type") reactionType: String, ): RetrofitCall - @GET("/messages/{id}/reactions") + @GET("/api/v2/chat/messages/{id}/reactions") fun getReactions( @Path("id") messageId: String, @Query("offset") offset: Int, @Query("limit") limit: Int, ): RetrofitCall - @POST("/messages/{id}/reactions") + @POST("/api/v2/chat/messages/{id}/reactions") fun queryReactions( @Path("id") messageId: String, @Body body: QueryReactionsRequest, ): RetrofitCall - @POST("/messages/{messageId}/translate") + @POST("/api/v2/chat/messages/{messageId}/translate") fun translate( @Path("messageId") messageId: String, @Body request: TranslateMessageRequest, ): RetrofitCall - @GET("/messages/{parent_id}/replies") + @GET("/api/v2/chat/messages/{parent_id}/replies") fun getReplies( @Path("parent_id") messageId: String, @Query("limit") limit: Int, ): RetrofitCall - @GET("/messages/{parent_id}/replies?sort=[{\"field\":\"created_at\",\"direction\":1}]") + @GET("/api/v2/chat/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 - @GET("/messages/{parent_id}/replies") + @GET("/api/v2/chat/messages/{parent_id}/replies") fun getRepliesMore( @Path("parent_id") messageId: String, @Query("limit") limit: Int, @Query("id_lt") firstId: String, ): RetrofitCall - @GET("/messages/{parent_id}/replies") + @GET("/api/v2/chat/messages/{parent_id}/replies") fun getRepliesAround( @Path("parent_id") parentId: String, @Query("limit") limit: Int, 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 89c32dfebbc3..1c8f1651ac71 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 @@ -135,6 +135,9 @@ internal class DtoMapping( private fun MutableMap.takeString(key: String): String? = takeTyped(key) { it as? String } + private fun MutableMap.takeStrings(key: String): List? = + takeTyped(key) { list -> (list as List<*>).map { it as String } } + private fun MutableMap.takeList(key: String, adapter: JsonAdapter): List? = takeTyped(key) { list -> (list as List<*>).map { adapter.fromJsonValue(it)!! } } @@ -230,6 +233,12 @@ internal class DtoMapping( messageTransformer.transform(this) .run { val upstreamType = if (type in supportedUpstreamMessageTypes) type else "" + // Responses also copy these declared fields into extraData (see DomainMapping.withDeclared), so a + // message sent back carries them. They go back to their typed fields, taking precedence as they did + // when v1 flattened extraData over the root. The response-only ones are dropped, since the backend + // would store them as custom data on either version. + val custom = extraData.toMutableMap() + custom.keys.removeAll(RESPONSE_ONLY_MESSAGE_KEYS) MessageRequest( id = id, text = text, @@ -238,8 +247,10 @@ internal class DtoMapping( mentionedUsers = mentionedUsersIds, mentionedHere = mentionedHere, mentionedChannel = mentionedChannel, - mentionedGroupIds = mentionedGroups.map(UserGroup::id), + mentionedGroupIds = custom.takeStrings("mentioned_group_ids") ?: mentionedGroups.map(UserGroup::id), mentionedRoles = mentionedRoles, + mml = custom.takeString("mml"), + pollId = custom.takeString("poll_id"), parentId = parentId, pinExpires = pinExpires, pinned = pinned, @@ -247,9 +258,9 @@ internal class DtoMapping( quotedMessageId = replyMessageId, showInChannel = showInChannel, silent = silent, - restrictedVisibility = restrictedVisibility, + restrictedVisibility = custom.takeStrings("restricted_visibility") ?: restrictedVisibility, sharedLocation = sharedLocation?.toSharedLocation(), - custom = extraData, + custom = custom, ) } @@ -360,3 +371,10 @@ internal class DtoMapping( defaultPreference = defaultPreference?.value?.let(ChatPreferencesInput.DefaultPreference::fromString), ) } + +/** + * Declared message fields that responses also copy into extraData but a message request doesn't carry. Sent back, the + * backend stores them as custom data: hidden from v1 readers, where the name clashes with the field, and shown to v2 + * readers under `custom`. + */ +private val RESPONSE_ONLY_MESSAGE_KEYS = setOf("draft", "image_labels") diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt index d745fc9819db..7e4cc1a0556a 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt @@ -349,7 +349,7 @@ constructor( fileTransformer = fileTransformer, buildV2RetrofitApi(), buildV2RetrofitApi(), - buildRetrofitApi(), + buildV2RetrofitApi(), buildV2RetrofitApi(), buildRetrofitApi(), buildV2RetrofitApi(), diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DtoMappingTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DtoMappingTest.kt index a6798bbb89bb..6415e6251f28 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DtoMappingTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DtoMappingTest.kt @@ -282,6 +282,35 @@ internal class DtoMappingTest { verify(messageTransformer, times(1)).transform(message) } + @Test + fun `Message sends the declared fields it carries in extraData as typed fields`() { + // Responses keep these in extraData, and sendPoll passes the poll id there. + val message = randomMessage( + type = MessageType.REGULAR, + extraData = mutableMapOf( + "poll_id" to "poll", + "mml" to "", + "restricted_visibility" to listOf("amit"), + "mentioned_group_ids" to listOf("group"), + "draft" to mapOf("channel_cid" to "messaging:a"), + "image_labels" to mapOf("i" to listOf("cat")), + "messageKey" to "messageValue", + ), + restrictedVisibility = emptyList(), + mentionedGroups = emptyList(), + ) + val mapping = Fixture().get() + + val request = with(mapping) { message.toMessageRequest() } + + request.pollId shouldBeEqualTo "poll" + request.mml shouldBeEqualTo "" + request.restrictedVisibility shouldBeEqualTo listOf("amit") + request.mentionedGroupIds shouldBeEqualTo listOf("group") + // The response-only ones are not sent, so they are not stored as custom data. + request.custom shouldBeEqualTo mapOf("messageKey" to "messageValue") + } + @ParameterizedTest @MethodSource("messageTypeCoercionInput") fun `Message toMessageRequest coerces type to allowed upstream values`( diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/network/infrastructure/V2LaneBatchFiveTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/network/infrastructure/V2LaneBatchFiveTest.kt new file mode 100644 index 000000000000..0255a5516726 --- /dev/null +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/network/infrastructure/V2LaneBatchFiveTest.kt @@ -0,0 +1,190 @@ +/* + * 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.network.infrastructure + +import io.getstream.chat.android.client.api2.mapping.DomainMapping +import io.getstream.chat.android.client.parser2.ParserFactory +import io.getstream.chat.android.models.NoOpChannelTransformer +import io.getstream.chat.android.models.NoOpMessageTransformer +import io.getstream.chat.android.models.NoOpUserTransformer +import io.getstream.chat.android.network.models.DeleteMessageResponse +import io.getstream.chat.android.network.models.GetMessageResponse +import io.getstream.chat.android.network.models.GetRepliesResponse +import io.getstream.chat.android.network.models.MessageActionRequest +import io.getstream.chat.android.network.models.MessageRequest +import io.getstream.chat.android.network.models.QueryReactionsRequest +import io.getstream.chat.android.network.models.ReactionRequest +import io.getstream.chat.android.network.models.SendMessageRequest +import io.getstream.chat.android.network.models.SendMessageResponse +import io.getstream.chat.android.network.models.SendReactionRequest +import io.getstream.chat.android.network.models.SendReactionResponse +import io.getstream.chat.android.network.models.SortParamRequest +import io.getstream.chat.android.network.models.TranslateMessageRequest +import io.getstream.chat.android.network.models.TranslateMessageResponse +import io.getstream.chat.android.network.models.UpdateMessagePartialRequest +import io.getstream.chat.android.network.models.UpdateMessageRequest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Date + +/** + * The message endpoints (send, get, update, delete, reactions, replies, translate, actions) parse their recorded v2 + * responses through the generated [Serializer] alone. v2 reads message and reaction custom data only when it is nested + * under `custom`, so those requests keep it nested; the other request bodies are the same as on v1. + */ +internal class V2LaneBatchFiveTest { + + private val v1Parser = ParserFactory.createMoshiChatParser() + private val mapping = DomainMapping( + currentUserIdProvider = { "filip" }, + channelTransformer = NoOpChannelTransformer, + messageTransformer = NoOpMessageTransformer, + userTransformer = NoOpUserTransformer, + ) + + private inline fun parse(json: String): T = Serializer.moshi.adapter(T::class.java).fromJson(json)!! + + private inline fun v2Body(request: T): String = Serializer.moshi.adapter(T::class.java).toJson(request) + + @Test + fun `A sent message parses its nested custom data and integer dates from v2`() { + val message = with(mapping) { parse(SENT).message.toDomain() } + + assertEquals(mapOf("msg_probe" to "nested"), message.extraData) + assertEquals(Date(1791797467710), message.createdAt) + assertEquals(Date(1634853490403), message.user.createdAt) + } + + @Test + fun `A fetched message parses its channel's name and image from the channel's nested custom data`() { + val message = with(mapping) { parse(FETCHED).message.toDomain() } + + assertEquals("Silent Active", message.channelInfo?.name) + assertEquals(mapOf("msg_probe" to "nested"), message.extraData) + } + + @Test + fun `Replies and a deleted message parse from v2`() { + val reply = message("v2cap reply", "{}") + val replies = parse("""{"messages":[$reply],"duration":"19.07ms"}""") + val gone = message("v2cap msg 3", "{}") + val deleted = parse("""{"message":$gone,"duration":"32.91ms"}""") + + with(mapping) { + assertEquals(Date(1791797467710), replies.messages.single().toDomain().createdAt) + assertEquals("v2cap msg 3", deleted.message.toDomain().text) + } + } + + @Test + fun `A reaction parses its nested custom data from v2`() { + val reaction = with(mapping) { parse(REACTION).reaction.toDomain() } + + assertEquals(mapOf("r_probe" to "nested"), reaction.extraData) + assertEquals(Date(1791797468742), reaction.createdAt) + } + + @Test + fun `A translation parses from v2`() { + val message = with(mapping) { parse(TRANSLATED).message.toDomain() } + + assertEquals(mapOf("en_text" to "v2cap msg 3", "it_text" to "v2cap msg 3", "language" to "en"), message.i18n) + } + + @Test + fun `A sent message nests custom data on v2, where the v1 body would lose it`() { + val request = SendMessageRequest(MessageRequest(text = "Hi", custom = mapOf("k" to "v"))) + val lists = """"attachments":[],"mentioned_group_ids":[],"mentioned_roles":[],"mentioned_users":[],""" + + """"restricted_visibility":[]""" + + assertEquals("""{"message":{"text":"Hi",$lists,"custom":{"k":"v"}}}""", v2Body(request)) + // v1 flattens custom data to the message's root, which a v2 endpoint silently drops. + assertEquals("""{"message":{"text":"Hi",$lists,"k":"v"}}""", v1Parser.toJson(request)) + } + + @Test + fun `An updated message nests custom data on v2`() { + val request = UpdateMessageRequest(MessageRequest(id = "m", text = "Hi", custom = mapOf("k" to "v"))) + + assertEquals( + """{"message":{"id":"m","text":"Hi","attachments":[],"mentioned_group_ids":[],"mentioned_roles":[],""" + + """"mentioned_users":[],"restricted_visibility":[],"custom":{"k":"v"}}}""", + v2Body(request), + ) + } + + @Test + fun `A reaction nests custom data on v2, where the v1 body would lose it`() { + val reaction = ReactionRequest(type = "like", custom = mapOf("k" to "v")) + val request = SendReactionRequest(reaction, enforceUnique = true) + + assertEquals("""{"reaction":{"type":"like","custom":{"k":"v"}},"enforce_unique":true}""", v2Body(request)) + // v1 flattens custom data to the reaction's root, which a v2 endpoint silently drops. + assertEquals("""{"reaction":{"type":"like","k":"v"},"enforce_unique":true}""", v1Parser.toJson(request)) + } + + @Test + fun `A partial message update has the same body on v2 as on v1`() { + // Root keys of `set` are custom data on both versions. + assertSameBody(UpdateMessagePartialRequest(set = mapOf("text" to "Hi", "k" to "v"), unset = listOf("q"))) + } + + @Test + fun `Reaction queries, actions and translations have the same bodies on v2 as on v1`() { + val sort = listOf(SortParamRequest(field = "created_at", direction = -1)) + assertSameBody(QueryReactionsRequest(filter = mapOf("type" to "like"), limit = 5, sort = sort)) + assertSameBody(MessageActionRequest(formData = mapOf("image_action" to "shuffle"))) + assertSameBody(TranslateMessageRequest(TranslateMessageRequest.Language.fromString("it"))) + } + + private inline fun assertSameBody(request: T) { + assertEquals(v1Parser.toJson(request), v2Body(request)) + } + + private companion object { + private const val USER = """{"id":"filip","name":"Chewbacca","custom":{},"language":"","role":"user", + "teams":[],"created_at":1634853490403855000,"updated_at":1791791284528654000,"banned":false, + "online":true,"blocked_user_ids":[]}""" + + private fun message(text: String, custom: String, i18n: String = "") = """{ + "id":"bad03680-ef84-45dd-ae67-e5f9ee871dff","text":"$text","html":"

$text

\n","type":"regular", + "user":$USER,"attachments":[],"latest_reactions":[],"own_reactions":[],"reaction_counts":{}, + "reaction_scores":{},"reply_count":0,"deleted_reply_count":0,"cid":"messaging:sample-app-channel-8", + "created_at":1791797467710999000,"updated_at":1791797467710999000,"custom":$custom,$i18n"shadowed":false, + "mentioned_users":[],"mentioned_channel":false,"mentioned_here":false,"silent":false,"pinned":false, + "restricted_visibility":[]}""" + + private val SENT = """{"message":${message("v2cap msg", """{"msg_probe":"nested"}""")},"duration":"32.50ms"}""" + + private val FETCHED = """{"message":${message("v2cap msg", """{"msg_probe":"nested"}""").dropLast(1)}, + "channel":{"id":"sample-app-channel-8","type":"messaging","cid":"messaging:sample-app-channel-8", + "last_message_at":1791797467710999000,"created_at":1634854188377760000,"updated_at":1785561493155836000, + "frozen":false,"disabled":false,"member_count":13,"truncated_at":1636539502702175000, + "custom":{"name":"Silent Active","image":"https://picsum.photos/seed/72/100/100"}}},"duration":"13.05ms"}""" + + private val TRANSLATED = """{"message":${message( + "v2cap msg 3", + """{"partial_probe":"root"}""", + """"i18n":{"en_text":"v2cap msg 3","it_text":"v2cap msg 3","language":"en"},""", + )},"duration":"214.63ms"}""" + + private val REACTION = """{"message":${message("v2cap msg 3", """{"partial_probe":"root"}""")}, + "reaction":{"message_id":"bad03680-ef84-45dd-ae67-e5f9ee871dff","user_id":"filip","user":$USER, + "type":"like","score":1,"created_at":1791797468742013000,"updated_at":1791797468742013000, + "custom":{"r_probe":"nested"}},"duration":"28.71ms"}""" + } +} diff --git a/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/BaseUiTest.kt b/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/BaseUiTest.kt index 0e7ba4ae6f4c..abfe7824440f 100644 --- a/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/BaseUiTest.kt +++ b/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/BaseUiTest.kt @@ -89,7 +89,7 @@ internal abstract class BaseUiTest { okResponse(readFileContents(HTTP_CHANNELS)) } else if (path.startsWith("/channels/messaging/general/query")) { okResponse(readFileContents(HTTP_CHANNEL)) - } else if (path.startsWith("/channels/messaging/general/message")) { + } else if (path.startsWith("/api/v2/chat/channels/messaging/general/message")) { val messageId = JSONObject(request.body.readUtf8()).getString("id") webSocket.send(readFileContents(WS_MESSAGE_NEW).replace(":messageId", messageId)) okResponse(readFileContents(HTTP_MESSAGE).replace(":messageId", messageId)) From 4d1a413e5a18f8447f0a95a837ed1d366407d37a Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:04:49 +0200 Subject: [PATCH 2/2] Derive the message fields a request drops from the ones responses copy to extraData --- .../android/client/api2/mapping/DtoMapping.kt | 22 ++++++++++++++----- .../client/api2/mapping/DtoMappingTest.kt | 6 +++++ 2 files changed, 22 insertions(+), 6 deletions(-) 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 1c8f1651ac71..6f1eb76fd372 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 @@ -24,6 +24,7 @@ import io.getstream.chat.android.client.api2.model.dto.DeviceDto import io.getstream.chat.android.client.api2.model.dto.UpstreamConnectedEventDto import io.getstream.chat.android.client.api2.model.dto.UpstreamUserDto import io.getstream.chat.android.client.events.ConnectedEvent +import io.getstream.chat.android.client.parser2.adapters.GENERATED_MESSAGE_EXTRA_DATA_KEYS import io.getstream.chat.android.client.parser2.adapters.internal.StreamDateFormatter import io.getstream.chat.android.models.Attachment import io.getstream.chat.android.models.ChatPreferences @@ -247,10 +248,11 @@ internal class DtoMapping( mentionedUsers = mentionedUsersIds, mentionedHere = mentionedHere, mentionedChannel = mentionedChannel, - mentionedGroupIds = custom.takeStrings("mentioned_group_ids") ?: mentionedGroups.map(UserGroup::id), + mentionedGroupIds = custom.takeStrings(MENTIONED_GROUP_IDS_KEY) + ?: mentionedGroups.map(UserGroup::id), mentionedRoles = mentionedRoles, - mml = custom.takeString("mml"), - pollId = custom.takeString("poll_id"), + mml = custom.takeString(MML_KEY), + pollId = custom.takeString(POLL_ID_KEY), parentId = parentId, pinExpires = pinExpires, pinned = pinned, @@ -258,7 +260,7 @@ internal class DtoMapping( quotedMessageId = replyMessageId, showInChannel = showInChannel, silent = silent, - restrictedVisibility = custom.takeStrings("restricted_visibility") ?: restrictedVisibility, + restrictedVisibility = custom.takeStrings(RESTRICTED_VISIBILITY_KEY) ?: restrictedVisibility, sharedLocation = sharedLocation?.toSharedLocation(), custom = custom, ) @@ -372,9 +374,17 @@ internal class DtoMapping( ) } +private const val MENTIONED_GROUP_IDS_KEY = "mentioned_group_ids" +private const val MML_KEY = "mml" +private const val POLL_ID_KEY = "poll_id" +private const val RESTRICTED_VISIBILITY_KEY = "restricted_visibility" + +/** The declared message fields responses copy into extraData that a message request sends back as typed fields. */ +internal val SENT_BACK_MESSAGE_KEYS = setOf(MENTIONED_GROUP_IDS_KEY, MML_KEY, POLL_ID_KEY, RESTRICTED_VISIBILITY_KEY) + /** * Declared message fields that responses also copy into extraData but a message request doesn't carry. Sent back, the * backend stores them as custom data: hidden from v1 readers, where the name clashes with the field, and shown to v2 - * readers under `custom`. + * readers under `custom`. Derived, so a field added to the copied ones is dropped until a request carries it. */ -private val RESPONSE_ONLY_MESSAGE_KEYS = setOf("draft", "image_labels") +private val RESPONSE_ONLY_MESSAGE_KEYS = GENERATED_MESSAGE_EXTRA_DATA_KEYS - SENT_BACK_MESSAGE_KEYS diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DtoMappingTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DtoMappingTest.kt index 6415e6251f28..af8c6cc0c502 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DtoMappingTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/DtoMappingTest.kt @@ -23,6 +23,7 @@ import io.getstream.chat.android.TypingIndicators import io.getstream.chat.android.client.api2.model.dto.DeviceDto import io.getstream.chat.android.client.api2.model.dto.UpstreamConnectedEventDto import io.getstream.chat.android.client.api2.model.dto.UpstreamUserDto +import io.getstream.chat.android.client.parser2.adapters.GENERATED_MESSAGE_EXTRA_DATA_KEYS import io.getstream.chat.android.client.test.randomConnectedEvent import io.getstream.chat.android.models.MessageTransformer import io.getstream.chat.android.models.MessageType @@ -546,4 +547,9 @@ internal class DtoMappingTest { userTransformer = userTransformer, ) } + + @Test + fun `The fields a message request sends back typed are among the declared fields responses copy to extraData`() { + (SENT_BACK_MESSAGE_KEYS - GENERATED_MESSAGE_EXTRA_DATA_KEYS) shouldBeEqualTo emptySet() + } }