From 86b27520701b4ec07bff8a12dbbfba8ea4b52f21 Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Sat, 21 Feb 2026 17:51:32 +0100 Subject: [PATCH] Fix delivery receipts not being sent in privacy settings updates Co-Authored-By: Claude --- .../android/client/api2/mapping/DtoMapping.kt | 10 +++ .../android/client/socket/SocketFactory.kt | 21 ++--- .../client/api2/mapping/DtoMappingTest.kt | 21 +++++ .../client/socket/SocketFactoryTest.kt | 80 +++++++++++++++---- 4 files changed, 103 insertions(+), 29 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 39205468738..3439c354cf0 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 @@ -16,10 +16,12 @@ package io.getstream.chat.android.client.api2.mapping +import io.getstream.chat.android.DeliveryReceipts import io.getstream.chat.android.PrivacySettings import io.getstream.chat.android.ReadReceipts import io.getstream.chat.android.TypingIndicators import io.getstream.chat.android.client.api2.model.dto.AttachmentDto +import io.getstream.chat.android.client.api2.model.dto.DeliveryReceiptsDto import io.getstream.chat.android.client.api2.model.dto.DeviceDto import io.getstream.chat.android.client.api2.model.dto.PrivacySettingsDto import io.getstream.chat.android.client.api2.model.dto.ReadReceiptsDto @@ -207,6 +209,7 @@ internal class DtoMapping( internal fun PrivacySettings.toDto(): PrivacySettingsDto = PrivacySettingsDto( typing_indicators = typingIndicators?.toDto(), read_receipts = readReceipts?.toDto(), + delivery_receipts = deliveryReceipts?.toDto(), ) /** @@ -223,6 +226,13 @@ internal class DtoMapping( enabled = enabled, ) + /** + * Maps the domain [DeliveryReceipts] model to a network [DeliveryReceiptsDto] model. + */ + internal fun DeliveryReceipts.toDto(): DeliveryReceiptsDto = DeliveryReceiptsDto( + enabled = enabled, + ) + /** * Maps the domain [User] model to a network [UpstreamUserDto] model. * diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/socket/SocketFactory.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/socket/SocketFactory.kt index 196fa19a1ee..582b1238d2f 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/socket/SocketFactory.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/socket/SocketFactory.kt @@ -107,21 +107,14 @@ internal class SocketFactory( private fun PrivacySettings.reducePrivacySettings(): Map = mutableMapOf() .apply { - typingIndicators?.also { - put( - "typing_indicators", - mapOf( - "enabled" to it.enabled, - ), - ) + typingIndicators?.let { + put("typing_indicators", mapOf("enabled" to it.enabled)) } - readReceipts?.also { - put( - "read_receipts", - mapOf( - "enabled" to it.enabled, - ), - ) + deliveryReceipts?.let { + put("delivery_receipts", mapOf("enabled" to it.enabled)) + } + readReceipts?.let { + put("read_receipts", mapOf("enabled" to it.enabled)) } } 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 0df358ab54c..d86fe3f1454 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 @@ -16,10 +16,12 @@ package io.getstream.chat.android.client.api2.mapping +import io.getstream.chat.android.DeliveryReceipts import io.getstream.chat.android.PrivacySettings import io.getstream.chat.android.ReadReceipts import io.getstream.chat.android.TypingIndicators import io.getstream.chat.android.client.api2.model.dto.AttachmentDto +import io.getstream.chat.android.client.api2.model.dto.DeliveryReceiptsDto import io.getstream.chat.android.client.api2.model.dto.DeviceDto import io.getstream.chat.android.client.api2.model.dto.PrivacySettingsDto import io.getstream.chat.android.client.api2.model.dto.ReadReceiptsDto @@ -238,12 +240,31 @@ internal class DtoMappingTest { val privacySettings = PrivacySettings( typingIndicators = TypingIndicators(enabled = true), readReceipts = ReadReceipts(enabled = false), + deliveryReceipts = DeliveryReceipts(enabled = false), ) val mapping = Fixture().get() val dto = with(mapping) { privacySettings.toDto() } val expected = PrivacySettingsDto( typing_indicators = TypingIndicatorsDto(enabled = true), read_receipts = ReadReceiptsDto(enabled = false), + delivery_receipts = DeliveryReceiptsDto(enabled = false), + ) + dto shouldBeEqualTo expected + } + + @Test + fun `PrivacySettings with null deliveryReceipts is correctly mapped to Dto`() { + val privacySettings = PrivacySettings( + typingIndicators = TypingIndicators(enabled = true), + readReceipts = ReadReceipts(enabled = false), + deliveryReceipts = null, + ) + val mapping = Fixture().get() + val dto = with(mapping) { privacySettings.toDto() } + val expected = PrivacySettingsDto( + typing_indicators = TypingIndicatorsDto(enabled = true), + read_receipts = ReadReceiptsDto(enabled = false), + delivery_receipts = null, ) dto shouldBeEqualTo expected } diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/socket/SocketFactoryTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/socket/SocketFactoryTest.kt index 41002a9d56c..3509bf8599e 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/socket/SocketFactoryTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/socket/SocketFactoryTest.kt @@ -16,6 +16,10 @@ package io.getstream.chat.android.client.socket +import io.getstream.chat.android.DeliveryReceipts +import io.getstream.chat.android.PrivacySettings +import io.getstream.chat.android.ReadReceipts +import io.getstream.chat.android.TypingIndicators import io.getstream.chat.android.client.parser.ChatParser import io.getstream.chat.android.client.parser2.ParserFactory import io.getstream.chat.android.client.token.FakeTokenManager @@ -94,7 +98,7 @@ internal class SocketFactoryTest { } @JvmStatic - @Suppress("MaxLineLength") + @Suppress("MaxLineLength", "LongMethod") fun arguments() = listOf( randomUser(image = randomString(), name = randomString(), language = randomString()).let { Arguments.of( @@ -138,26 +142,72 @@ internal class SocketFactoryTest { "${endpoint}connect?json=${buildMinimumUserJson("anon")}&api_key=$apiKey&X-Stream-Client=${headersUtil.buildSdkTrackingHeaders()}&stream-auth-type=anonymous", ) }, + randomUser( + image = randomString(), + name = randomString(), + language = randomString(), + privacySettings = PrivacySettings( + typingIndicators = TypingIndicators(enabled = false), + deliveryReceipts = DeliveryReceipts(enabled = false), + readReceipts = ReadReceipts(enabled = true), + ), + ).let { + Arguments.of( + false, + SocketFactory.ConnectionConf.UserConnectionConf(endpoint, apiKey, it), + "${endpoint}connect?json=${buildFullUserJson(it, it.id)}&api_key=$apiKey&X-Stream-Client=${headersUtil.buildSdkTrackingHeaders()}&authorization=$token&stream-auth-type=jwt", + ) + }, + randomUser( + image = randomString(), + name = randomString(), + language = randomString(), + privacySettings = PrivacySettings( + typingIndicators = null, + deliveryReceipts = DeliveryReceipts(enabled = false), + readReceipts = null, + ), + ).let { + Arguments.of( + false, + SocketFactory.ConnectionConf.UserConnectionConf(endpoint, apiKey, it), + "${endpoint}connect?json=${buildFullUserJson(it, it.id)}&api_key=$apiKey&X-Stream-Client=${headersUtil.buildSdkTrackingHeaders()}&authorization=$token&stream-auth-type=jwt", + ) + }, ) private fun buildMinimumUserJson(userId: String): String = encode( defaultMap(userId, mapOf("id" to userId)), ) - private fun buildFullUserJson(user: User, userId: String): String = encode( - defaultMap( - userId, - mapOf( - "id" to userId, - "role" to user.role, - "banned" to user.isBanned, - "invisible" to user.isInvisible, - "language" to user.language, - "image" to user.image, - "name" to user.name, - ) + user.extraData, - ), - ) + private fun buildFullUserJson(user: User, userId: String): String { + val ps = user.privacySettings + return encode( + defaultMap( + userId, + linkedMapOf( + "id" to userId, + "role" to user.role, + "banned" to user.isBanned, + "invisible" to user.isInvisible, + ) + if (ps != null) { + mapOf( + "privacy_settings" to listOfNotNull( + ps.typingIndicators?.let { "typing_indicators" to mapOf("enabled" to it.enabled) }, + ps.deliveryReceipts?.let { "delivery_receipts" to mapOf("enabled" to it.enabled) }, + ps.readReceipts?.let { "read_receipts" to mapOf("enabled" to it.enabled) }, + ).toMap(), + ) + } else { + emptyMap() + } + mapOf( + "language" to user.language, + "image" to user.image, + "name" to user.name, + ) + user.extraData, + ), + ) + } private fun defaultMap(userId: String, userDetails: Map): Map = mapOf(