Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import io.getstream.chat.android.client.api2.model.dto.DownstreamReminderDto
import io.getstream.chat.android.client.api2.model.dto.DownstreamReminderInfoDto
import io.getstream.chat.android.client.api2.model.dto.DownstreamThreadDto
import io.getstream.chat.android.client.api2.model.dto.DownstreamThreadInfoDto
import io.getstream.chat.android.client.api2.model.dto.DownstreamThreadParticipantDto
import io.getstream.chat.android.client.api2.model.dto.DownstreamUserBlockDto
import io.getstream.chat.android.client.api2.model.dto.DownstreamUserDto
import io.getstream.chat.android.client.api2.model.dto.DownstreamUserGroupDto
Expand Down Expand Up @@ -136,10 +135,12 @@ import io.getstream.chat.android.network.models.BlockUsersResponse
import io.getstream.chat.android.network.models.DeviceResponse
import io.getstream.chat.android.network.models.GetApplicationResponse
import io.getstream.chat.android.network.models.UserGroupResponse
import io.getstream.chat.android.network.models.UserResponse
import java.util.Date
import io.getstream.chat.android.network.models.Command as CommandDto
import io.getstream.chat.android.network.models.FileUploadConfig as UploadConfigDto
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

@Suppress("TooManyFunctions", "LargeClass")
Expand Down Expand Up @@ -439,6 +440,26 @@ internal class DomainMapping(
extraData = extraData,
)

internal fun UserResponse.toDomain(): User =
User(
id = id,
role = role,
name = name.orEmpty(),
image = image.orEmpty(),
language = language,
banned = banned,
online = online,
createdAt = createdAt,
updatedAt = updatedAt,
lastActive = lastActive,
deactivatedAt = deactivatedAt,
teams = teams,
teamsRole = teamsRole.orEmpty(),
blockedUserIds = blockedUserIds,
avgResponseTime = avgResponseTime?.toLong(),
extraData = custom.mapNotNull { (key, value) -> value?.let { key to it } }.toMap(),
).let(userTransformer::transform)

internal fun DownstreamLocationDto.toDomain(): Location =
Location(
cid = channel_cid,
Expand Down Expand Up @@ -837,11 +858,11 @@ internal class DomainMapping(
)

/**
* Transforms [DownstreamThreadParticipantDto] into [ThreadParticipant]
* Transforms [ThreadParticipantDto] into [ThreadParticipant]
*/
internal fun DownstreamThreadParticipantDto.toDomain(): ThreadParticipant = ThreadParticipant(
user = user?.toDomain() ?: User(id = user_id),
lastThreadMessageAt = last_thread_message_at,
internal fun ThreadParticipantDto.toDomain(): ThreadParticipant = ThreadParticipant(
user = user?.toDomain() ?: User(id = userId.orEmpty()),
lastThreadMessageAt = lastThreadMessageAt,
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package io.getstream.chat.android.client.api2.model.dto

import com.squareup.moshi.JsonClass
import io.getstream.chat.android.network.models.ThreadParticipant
import java.util.Date

/**
Expand Down Expand Up @@ -60,7 +61,7 @@
val participant_count: Int,
val read: List<DownstreamChannelUserRead>?,
val reply_count: Int?,
val thread_participants: List<DownstreamThreadParticipantDto>?,
val thread_participants: List<ThreadParticipant>?,

Check warning on line 64 in stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/ThreadDtos.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this parameter to match the regular expression ^`?[_a-z][a-zA-Z0-9]*`?$

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-android&issues=AZ_Wcc1aTdXMJj1tkbBT&open=AZ_Wcc1aTdXMJj1tkbBT&pullRequest=6620
val title: String,
val updated_at: Date,
val extraData: Map<String, Any>,
Expand Down Expand Up @@ -98,26 +99,11 @@
val reply_count: Int?,
val participant_count: Int?,
val active_participant_count: Int?,
val thread_participants: List<DownstreamThreadParticipantDto>?,
val thread_participants: List<ThreadParticipant>?,

Check warning on line 102 in stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/ThreadDtos.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this parameter to match the regular expression ^`?[_a-z][a-zA-Z0-9]*`?$

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-android&issues=AZ_Wcc1aTdXMJj1tkbBU&open=AZ_Wcc1aTdXMJj1tkbBU&pullRequest=6620
val last_message_at: Date?,
val created_at: Date,
val updated_at: Date,
val deleted_at: Date?,
val title: String,
val extraData: Map<String, Any>,
) : ExtraDataDto

/**
* The DTO for Thread Participant.
*
* @param user_id The ID of the user (thread participant).
* @param user The user as the thread participant. (Note: It is not always delivered, sometimes we only get the ID of
* the user - [user_id]).
* @param last_thread_message_at The date of the last message in the thread at the time of participation.
*/
@JsonClass(generateAdapter = true)
internal data class DownstreamThreadParticipantDto(
val user_id: String,
val user: DownstreamUserDto?,
val last_thread_message_at: Date?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import io.getstream.chat.android.client.parser2.adapters.DownstreamThreadInfoDto
import io.getstream.chat.android.client.parser2.adapters.DownstreamUserDtoAdapter
import io.getstream.chat.android.client.parser2.adapters.EventAdapterFactory
import io.getstream.chat.android.client.parser2.adapters.ExactDateAdapter
import io.getstream.chat.android.client.parser2.adapters.NullCollectionsAsEmptyFactory
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.UpdatePollOptionRequestAdapter
Expand All @@ -55,6 +56,7 @@ import io.getstream.chat.android.client.parser2.adapters.UpstreamMessageDtoAdapt
import io.getstream.chat.android.client.parser2.adapters.UpstreamReactionDtoAdapter
import io.getstream.chat.android.client.parser2.adapters.UpstreamUserDtoAdapter
import io.getstream.chat.android.client.parser2.adapters.UserRequestAdapter
import io.getstream.chat.android.client.parser2.adapters.UserResponseAdapter
import io.getstream.chat.android.client.socket.ErrorResponse
import io.getstream.chat.android.client.socket.SocketErrorMessage
import io.getstream.chat.android.network.infrastructure.Serializer
Expand Down Expand Up @@ -82,6 +84,7 @@ internal class MoshiChatParser(
.add(UpstreamReactionDtoAdapter)
.add(DownstreamUserDtoAdapter)
.add(UpstreamUserDtoAdapter)
.add(UserResponseAdapter)
.add(UserRequestAdapter)
.add(DownstreamMemberDtoAdapter)
.add(UpstreamMemberDtoAdapter)
Expand All @@ -105,6 +108,8 @@ internal class MoshiChatParser(
UpdatePollRequest.VotingVisibility::class.java,
UpdatePollRequest.VotingVisibility.VotingVisibilityAdapter(),
)
// Registered last so the model-specific adapters above keep precedence and delegate into it.
.add(NullCollectionsAsEmptyFactory)
.build()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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.Json
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.rawType
import java.lang.reflect.Type
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.primaryConstructor

/**
* Reads an explicit `null` as an empty collection for the non-null [List] and [Map] properties of
* the generated network models.
*
* The models are generated from the v2 spec, whose encoder writes nil Go maps and slices as `{}`
* and `[]`, so those properties are non-null. We call the v1 endpoints, whose encoder writes them
* as `null` instead (e.g. `thread_participants[].custom`), which would otherwise fail to parse.
*
* Remove once the client calls the v2 endpoints, where the shape already matches the models.
*/
internal object NullCollectionsAsEmptyFactory : JsonAdapter.Factory {

private const val GENERATED_MODELS_PACKAGE = "io.getstream.chat.android.network.models"

override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
if (annotations.isNotEmpty()) return null
val rawType = type.rawType
if (rawType.`package`?.name != GENERATED_MODELS_PACKAGE) return null

val emptyValues = emptyValuesByJsonName(rawType)
if (emptyValues.isEmpty()) return null

return NullCollectionsAsEmptyAdapter(
delegate = moshi.nextAdapter(this, type, annotations),
mapAdapter = moshi.adapter(
Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java),
),
emptyValues = emptyValues,
)
}

/**
* Wire names of the non-null collection properties of [rawType], mapped to their empty value.
*
* Relies on the Kotlin metadata of the generated models, which `consumer-proguard-rules.pro`
* keeps. Failures are deliberately not caught: swallowing them would silently stop coercing
* nulls and turn this into a release-only parsing crash.
*/
private fun emptyValuesByJsonName(rawType: Class<*>): Map<String, Any> =
rawType.kotlin.primaryConstructor?.parameters
.orEmpty()
.mapNotNull { parameter ->
if (parameter.type.isMarkedNullable) return@mapNotNull null
val empty: Any = when (parameter.type.classifier) {
List::class -> emptyList<Any?>()
Map::class -> emptyMap<String, Any?>()
else -> return@mapNotNull null
}
val name = parameter.findAnnotation<Json>()?.name ?: parameter.name ?: return@mapNotNull null
name to empty
}
.toMap()
}

private class NullCollectionsAsEmptyAdapter(
private val delegate: JsonAdapter<Any>,
private val mapAdapter: JsonAdapter<MutableMap<String, Any?>>,
private val emptyValues: Map<String, Any>,
) : JsonAdapter<Any>() {

override fun fromJson(reader: JsonReader): Any? {
if (reader.peek() == JsonReader.Token.NULL) {
return reader.nextNull()
}
val map = mapAdapter.fromJson(reader) ?: return null
emptyValues.forEach { (name, empty) ->
if (map.containsKey(name) && map[name] == null) {
map[name] = empty
}
}
return delegate.fromJsonValue(map)
}

override fun toJson(writer: JsonWriter, value: Any?) = delegate.toJson(writer, value)
}
Original file line number Diff line number Diff line change
@@ -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.
*/

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.UserResponse

// Downstream (read-only) adapter for the generated UserResponse: collects root-level custom fields
// into `custom`, matching the wire's flattened extra data. extraDataPropertyName is its @Json name.
internal object UserResponseAdapter :
CustomObjectDtoAdapter<UserResponse>(UserResponse::class, extraDataPropertyName = "custom") {

@FromJson
fun fromJson(
jsonReader: JsonReader,
mapAdapter: JsonAdapter<MutableMap<String, Any>>,
valueAdapter: JsonAdapter<UserResponse>,
): UserResponse? = parseWithExtraData(jsonReader, mapAdapter, valueAdapter)

@ToJson
@Suppress("UNUSED_PARAMETER")
fun toJson(jsonWriter: JsonWriter, value: UserResponse): Unit = error("Can't convert this to Json")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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

/**
* Represents a user that is participating in a thread.
*/
@com.squareup.moshi.JsonClass(generateAdapter = true)
internal data class ThreadParticipant(
@Json(name = "app_pk")
internal val appPk: Int,

@Json(name = "channel_cid")
internal val channelCid: String,

@Json(name = "created_at")
internal val createdAt: java.util.Date,

@Json(name = "last_read_at")
internal val lastReadAt: java.util.Date,

@Json(name = "custom")
internal val custom: Map<String, Any?> = emptyMap(),

@Json(name = "last_thread_message_at")
internal val lastThreadMessageAt: java.util.Date? = null,

@Json(name = "left_thread_at")
internal val leftThreadAt: java.util.Date? = null,

@Json(name = "thread_id")
internal val threadId: String? = null,

@Json(name = "user_id")
internal val userId: String? = null,

@Json(name = "user")
internal val user: io.getstream.chat.android.network.models.UserResponse? = null,
)
Loading
Loading