diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt index 52116590c176..d39b4cd95ad2 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt @@ -143,6 +143,7 @@ import io.getstream.chat.android.network.models.MessageRequest import io.getstream.chat.android.network.models.MuteChannelRequest import io.getstream.chat.android.network.models.PollOptionInput import io.getstream.chat.android.network.models.PollOptionRequest +import io.getstream.chat.android.network.models.PollVoteResponse import io.getstream.chat.android.network.models.PushPreferenceInput import io.getstream.chat.android.network.models.QueryDraftsRequest import io.getstream.chat.android.network.models.QueryMembersPayload @@ -1814,14 +1815,14 @@ constructor( messageId, pollId, CastPollVoteRequest(vote), - ).mapDomain { it.vote.toDomain() } + ).flatMapDomain { toVoteCall(it) } override fun removePollVote(messageId: String, pollId: String, voteId: String): Call = pollsApi.removePollVote( messageId, pollId, voteId, - ).mapDomain { it.vote.toDomain() } + ).flatMapDomain { toVoteCall(it) } override fun partialUpdatePoll(pollId: String, set: Map, unset: List): Call { val request = UpdatePollPartialRequest(set = set, unset = unset) @@ -2070,6 +2071,20 @@ constructor( } } + /** + * The vote is optional in the response schema, so a missing one is surfaced as a failure rather + * than crashing the mapping. + */ + private fun DomainMapping.toVoteCall(response: PollVoteResponse): Call { + val vote = response.vote + return if (vote != null) { + CoroutineCall(coroutineScope) { Result.Success(vote.toDomain()) } + } else { + val error = Error.GenericError("The vote is missing from the response") + ErrorCall(coroutineScope, error) + } + } + private fun RetrofitCall.flatMapDomain(transform: DomainMapping.(T) -> Call): Call = flatMap { domainMapping.transform(it) } } diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/PollsApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/PollsApi.kt index dbcb0c3216aa..2783b97953ad 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/PollsApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/PollsApi.kt @@ -17,17 +17,17 @@ 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.PollResponse -import io.getstream.chat.android.client.api2.model.response.PollVoteResponse -import io.getstream.chat.android.client.api2.model.response.QueryPollVotesResponse -import io.getstream.chat.android.client.api2.model.response.QueryPollsResponse import io.getstream.chat.android.client.call.RetrofitCall import io.getstream.chat.android.network.models.CastPollVoteRequest import io.getstream.chat.android.network.models.CreatePollOptionRequest import io.getstream.chat.android.network.models.CreatePollRequest import io.getstream.chat.android.network.models.PollOptionResponse +import io.getstream.chat.android.network.models.PollResponse +import io.getstream.chat.android.network.models.PollVoteResponse +import io.getstream.chat.android.network.models.PollVotesResponse 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.Response import io.getstream.chat.android.network.models.UpdatePollOptionRequest import io.getstream.chat.android.network.models.UpdatePollPartialRequest @@ -165,7 +165,7 @@ internal interface PollsApi { fun queryPollVotes( @Path("poll_id") pollId: String, @Body body: QueryPollVotesRequest, - ): RetrofitCall + ): RetrofitCall /** * Queries polls based on the provided criteria. 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 012772277d95..5fd75c4c8017 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 @@ -57,8 +57,6 @@ import io.getstream.chat.android.client.api2.model.dto.ReadReceiptsDto import io.getstream.chat.android.client.api2.model.dto.SearchWarningDto import io.getstream.chat.android.client.api2.model.dto.TypingIndicatorsDto import io.getstream.chat.android.client.api2.model.response.MessageResponse -import io.getstream.chat.android.client.api2.model.response.QueryPollVotesResponse -import io.getstream.chat.android.client.api2.model.response.QueryPollsResponse import io.getstream.chat.android.client.api2.model.response.QueryRemindersResponse import io.getstream.chat.android.client.extensions.enrichWithCid import io.getstream.chat.android.client.extensions.internal.sortedByLastReply @@ -141,8 +139,12 @@ import io.getstream.chat.android.network.models.FullUserResponse import io.getstream.chat.android.network.models.GetApplicationResponse import io.getstream.chat.android.network.models.GetOGResponse import io.getstream.chat.android.network.models.PollOptionResponseData +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.PrivacySettingsResponse import io.getstream.chat.android.network.models.PushPreferencesResponse +import io.getstream.chat.android.network.models.QueryPollsResponse import io.getstream.chat.android.network.models.UnreadCountsChannel import io.getstream.chat.android.network.models.UnreadCountsChannelType import io.getstream.chat.android.network.models.UnreadCountsThread @@ -676,6 +678,101 @@ internal class DomainMapping( ) } + /** + * Transforms [PollResponseData] into [Poll] + */ + internal fun PollResponseData.toDomain(): Poll { + val ownUserId = currentUserIdProvider() ?: ownVotes.firstOrNull()?.user?.id + val allVotes = latestVotesByOption.values + .flatten() + .filter { it.isAnswer != true } + .map { it.toDomain() } + val resolvedOwnVotes = ( + ownVotes + .filter { it.isAnswer != true } + .map { it.toDomain() } + + allVotes.filter { it.user?.id == ownUserId } + ) + .associateBy { it.id } + .values + .toList() + + return Poll( + id = id, + name = name, + description = description, + options = options.map { it.toOption() }, + votingVisibility = votingVisibility.toVotingVisibility(), + enforceUniqueVote = enforceUniqueVote, + maxVotesAllowed = maxVotesAllowed, + allowUserSuggestedOptions = allowUserSuggestedOptions, + allowAnswers = allowAnswers, + voteCount = voteCount, + voteCountsByOption = voteCountsByOption, + votes = allVotes, + ownVotes = resolvedOwnVotes, + createdAt = createdAt, + updatedAt = updatedAt, + closed = isClosed ?: false, + answersCount = answersCount, + answers = latestAnswers.map { it.toAnswerDomain() }, + createdBy = createdBy?.toDomain(), + extraData = custom.mapNotNull { (key, value) -> value?.let { key to it } }.toMap(), + ) + } + + /** + * Transforms [PollVoteResponseData] into [Vote] + */ + internal fun PollVoteResponseData.toDomain(): Vote = Vote( + id = id, + pollId = pollId, + optionId = optionId, + createdAt = createdAt, + updatedAt = updatedAt, + user = user?.toDomain(), + ) + + /** + * Transforms [PollVoteResponseData] into [Answer] + */ + internal fun PollVoteResponseData.toAnswerDomain(): Answer = Answer( + id = id, + pollId = pollId, + text = answerText ?: "", + createdAt = createdAt, + updatedAt = updatedAt, + user = user?.toDomain(), + ) + + /** + * Transforms [PollVotesResponse] into [QueryPollVotesResult] + */ + internal fun PollVotesResponse.toDomain(): QueryPollVotesResult = QueryPollVotesResult( + votes = votes.map { it.toDomain() }, + next = next, + ) + + /** + * Transforms [QueryPollsResponse] into [QueryPollsResult] + */ + internal fun QueryPollsResponse.toDomain(): QueryPollsResult = QueryPollsResult( + polls = polls.map { it.toDomain() }, + next = next, + ) + + /** + * Transforms [PollOptionResponseData] to [Option]. + * + * Not named `toDomain` because that maps to [PollOption], mirroring the hand-written + * [DownstreamPollOptionDto.toDomain] / [DownstreamPollOptionDto.toPollOption] pair. + */ + internal fun PollOptionResponseData.toOption(): Option = Option( + id = id, + text = text, + extraData = custom.mapNotNull { (key, value) -> value?.let { key to it } }.toMap(), + ) + /** * Transforms [PollOptionResponseData] into [PollOption] */ @@ -748,22 +845,6 @@ internal class DomainMapping( else -> throw IllegalArgumentException("Unknown voting visibility: $this") } - /** - * Transforms [QueryPollVotesResponse] to [QueryPollVotesResult]. - */ - internal fun QueryPollVotesResponse.toDomain(): QueryPollVotesResult = QueryPollVotesResult( - votes = votes.map { it.toDomain() }, - next = next, - ) - - /** - * Transforms the network [QueryPollsResponse] to a domain [QueryPollsResult]. - */ - internal fun QueryPollsResponse.toDomain(): QueryPollsResult = QueryPollsResult( - polls = polls.map { it.toDomain() }, - next = next, - ) - /** * Transform [DownstreamChannelUserRead] to [ChannelUserRead]. * 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 19f2fa558dfe..fa2ee625c308 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 @@ -59,6 +59,7 @@ import io.getstream.chat.android.client.parser2.adapters.NullCollectionsAsEmptyF 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.UpdatePollOptionRequestAdapter import io.getstream.chat.android.client.parser2.adapters.UpdatePollRequestAdapter import io.getstream.chat.android.client.parser2.adapters.UpstreamMemberDataDtoAdapter @@ -129,6 +130,7 @@ internal class MoshiChatParser( .add(EventRequestAdapter) .add(GetOGResponseAdapter) .add(PollOptionResponseDataAdapter) + .add(PollResponseDataAdapter) .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/PollResponseDataAdapter.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/PollResponseDataAdapter.kt new file mode 100644 index 000000000000..e69057e48988 --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/PollResponseDataAdapter.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.PollResponseData + +/** + * Collects the root-level custom fields of a poll into `custom`, which is how the API sends + * them. + */ +internal object PollResponseDataAdapter : + CustomObjectDtoAdapter( + PollResponseData::class, + extraDataPropertyName = "custom", + ) { + + @FromJson + fun fromJson( + jsonReader: JsonReader, + mapAdapter: JsonAdapter>, + valueAdapter: JsonAdapter, + ): PollResponseData? = parseWithExtraData(jsonReader, mapAdapter, valueAdapter) + + @ToJson + @Suppress("UNUSED_PARAMETER") + fun toJson(jsonWriter: JsonWriter, value: PollResponseData): 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/PollResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollResponse.kt similarity index 63% rename from stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/PollResponse.kt rename to stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollResponse.kt index c51704414718..7f9fb7edd04f 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/PollResponse.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollResponse.kt @@ -14,19 +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 io.getstream.chat.android.client.api2.model.dto.DownstreamPollDto +import com.squareup.moshi.Json /** - * Response for creating a new poll. * - * @property duration The duration of the request. - * @property poll The poll object. */ -@JsonClass(generateAdapter = true) +@com.squareup.moshi.JsonClass(generateAdapter = true) internal data class PollResponse( - val duration: String, - val poll: DownstreamPollDto, + @Json(name = "duration") + internal val duration: String, + + @Json(name = "poll") + internal val poll: PollResponseData, ) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollResponseData.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollResponseData.kt new file mode 100644 index 000000000000..bb9fa94dcc16 --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollResponseData.kt @@ -0,0 +1,95 @@ +/* + * 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 PollResponseData( + @Json(name = "allow_answers") + internal val allowAnswers: Boolean, + + @Json(name = "allow_user_suggested_options") + internal val allowUserSuggestedOptions: Boolean, + + @Json(name = "answers_count") + internal val answersCount: Int, + + @Json(name = "created_at") + internal val createdAt: java.util.Date, + + @Json(name = "created_by_id") + internal val createdById: String, + + @Json(name = "description") + internal val description: String, + + @Json(name = "enforce_unique_vote") + internal val enforceUniqueVote: Boolean, + + @Json(name = "id") + internal val id: String, + + @Json(name = "name") + internal val name: String, + + @Json(name = "updated_at") + internal val updatedAt: java.util.Date, + + @Json(name = "vote_count") + internal val voteCount: Int, + + @Json(name = "voting_visibility") + internal val votingVisibility: String, + + @Json(name = "latest_answers") + internal val latestAnswers: List = emptyList(), + + @Json(name = "options") + internal val options: List = emptyList(), + + @Json(name = "own_votes") + internal val ownVotes: List = emptyList(), + + @Json(name = "custom") + internal val custom: Map = emptyMap(), + + @Json(name = "latest_votes_by_option") + internal val latestVotesByOption: Map> = emptyMap(), + + @Json(name = "vote_counts_by_option") + internal val voteCountsByOption: Map = emptyMap(), + + @Json(name = "is_closed") + internal val isClosed: Boolean? = null, + + @Json(name = "max_votes_allowed") + internal val maxVotesAllowed: Int? = null, + + @Json(name = "created_by") + internal val createdBy: UserResponse? = null, +) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/PollVoteResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollVoteResponse.kt similarity index 58% rename from stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/PollVoteResponse.kt rename to stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollVoteResponse.kt index 866615fdb19f..05795abc5b90 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/PollVoteResponse.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollVoteResponse.kt @@ -14,19 +14,28 @@ * 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.DownstreamVoteDto +import com.squareup.moshi.Json /** - * Response for voting on a poll. * - * @property duration The duration of the request. - * @property vote The vote object. */ -@JsonClass(generateAdapter = true) +@com.squareup.moshi.JsonClass(generateAdapter = true) internal data class PollVoteResponse( - val duration: String, - val vote: DownstreamVoteDto, + @Json(name = "duration") + internal val duration: String, + + @Json(name = "poll") + internal val poll: PollResponseData? = null, + + @Json(name = "vote") + internal val vote: PollVoteResponseData? = null, ) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollVoteResponseData.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollVoteResponseData.kt new file mode 100644 index 000000000000..46169feda7f9 --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollVoteResponseData.kt @@ -0,0 +1,59 @@ +/* + * 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 PollVoteResponseData( + @Json(name = "created_at") + internal val createdAt: java.util.Date, + + @Json(name = "id") + internal val id: String, + + @Json(name = "option_id") + internal val optionId: String, + + @Json(name = "poll_id") + internal val pollId: String, + + @Json(name = "updated_at") + internal val updatedAt: java.util.Date, + + @Json(name = "answer_text") + internal val answerText: String? = null, + + @Json(name = "is_answer") + internal val isAnswer: Boolean? = null, + + @Json(name = "user_id") + internal val userId: String? = null, + + @Json(name = "user") + internal val user: UserResponse? = null, +) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/QueryPollVotesResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollVotesResponse.kt similarity index 51% rename from stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/QueryPollVotesResponse.kt rename to stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollVotesResponse.kt index b953263065e3..35cb147a66b7 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/QueryPollVotesResponse.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/PollVotesResponse.kt @@ -14,21 +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.DownstreamVoteDto +import com.squareup.moshi.Json /** - * Response for [io.getstream.chat.android.client.api2.endpoint.PollsApi.queryPollVotes] * - * @param next The identifier for the next page of poll votes. - * @param prev The identifier for the previous page of poll votes. - * @param votes The list of poll votes. */ -@JsonClass(generateAdapter = true) -internal data class QueryPollVotesResponse( - val next: String?, - val prev: String?, - val votes: List, +@com.squareup.moshi.JsonClass(generateAdapter = true) +internal data class PollVotesResponse( + @Json(name = "duration") + internal val duration: String, + + @Json(name = "votes") + internal val votes: 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/client/api2/model/response/QueryPollsResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/QueryPollsResponse.kt similarity index 55% rename from stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/QueryPollsResponse.kt rename to stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/QueryPollsResponse.kt index ad085be4d48f..447628afda8a 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/QueryPollsResponse.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/network/models/QueryPollsResponse.kt @@ -14,21 +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.DownstreamPollDto +import com.squareup.moshi.Json /** - * Response for [io.getstream.chat.android.client.api2.endpoint.PollsApi.queryPolls] * - * @param polls The list of polls. - * @param next The identifier for the next page of polls. - * @param prev The identifier for the previous page of polls. */ -@JsonClass(generateAdapter = true) +@com.squareup.moshi.JsonClass(generateAdapter = true) internal data class QueryPollsResponse( - val polls: List, - val next: String?, - val prev: String?, + @Json(name = "duration") + internal val duration: String, + + @Json(name = "polls") + internal val polls: 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/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 0566f01c16fb..0528b4868f2b 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 @@ -60,8 +60,6 @@ import io.getstream.chat.android.client.api2.model.dto.SearchWarningDto import io.getstream.chat.android.client.api2.model.dto.TypingIndicatorsDto import io.getstream.chat.android.client.api2.model.response.DraftMessageResponse import io.getstream.chat.android.client.api2.model.response.QueryDraftMessagesResponse -import io.getstream.chat.android.client.api2.model.response.QueryPollVotesResponse -import io.getstream.chat.android.client.api2.model.response.QueryPollsResponse import io.getstream.chat.android.client.api2.model.response.QueryRemindersResponse import io.getstream.chat.android.client.api2.model.response.SocketErrorResponse import io.getstream.chat.android.client.events.ConnectedEvent @@ -102,6 +100,10 @@ import io.getstream.chat.android.network.models.FullUserResponse import io.getstream.chat.android.network.models.GetApplicationResponse import io.getstream.chat.android.network.models.GetOGResponse import io.getstream.chat.android.network.models.PollOptionResponseData +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.ThreadParticipant import io.getstream.chat.android.network.models.UnblockUsersResponse import io.getstream.chat.android.network.models.UnreadCountsChannel @@ -1384,21 +1386,55 @@ internal object Mother { next = next, ) + fun randomPollResponseData( + id: String = randomString(), + name: String = randomString(), + custom: Map = emptyMap(), + ): PollResponseData = PollResponseData( + id = id, + name = name, + description = randomString(), + allowAnswers = randomBoolean(), + allowUserSuggestedOptions = randomBoolean(), + answersCount = randomInt(), + createdAt = randomDate(), + createdById = randomString(), + enforceUniqueVote = randomBoolean(), + updatedAt = randomDate(), + voteCount = randomInt(), + votingVisibility = "public", + custom = custom, + ) + + fun randomPollVoteResponseData( + id: String = randomString(), + pollId: String = randomString(), + optionId: String = randomString(), + ): PollVoteResponseData = PollVoteResponseData( + id = id, + pollId = pollId, + optionId = optionId, + createdAt = randomDate(), + updatedAt = randomDate(), + ) + fun randomQueryPollsResponse( - polls: List = listOf(randomDownstreamPollDto()), + polls: List = listOf(randomPollResponseData()), next: String? = randomString(), prev: String? = randomString(), ): QueryPollsResponse = QueryPollsResponse( + duration = randomString(), polls = polls, next = next, prev = prev, ) - fun randomQueryPollVotesResponse( - votes: List = listOf(randomDownstreamVoteDto()), + fun randomPollVotesResponse( + votes: List = listOf(randomPollVoteResponseData()), next: String? = randomString(), prev: String? = randomString(), - ): QueryPollVotesResponse = QueryPollVotesResponse( + ): PollVotesResponse = PollVotesResponse( + duration = randomString(), votes = votes, next = next, prev = prev, 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 e87c8576f8f4..c0923173ffd5 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 @@ -53,14 +53,10 @@ 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.ParsedPredefinedFilterResponse -import io.getstream.chat.android.client.api2.model.response.PollResponse -import io.getstream.chat.android.client.api2.model.response.PollVoteResponse import io.getstream.chat.android.client.api2.model.response.QueryChannelsResponse 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.QueryPollVotesResponse -import io.getstream.chat.android.client.api2.model.response.QueryPollsResponse 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 @@ -147,6 +143,9 @@ import io.getstream.chat.android.network.models.MuteChannelRequest import io.getstream.chat.android.network.models.PollOptionInput import io.getstream.chat.android.network.models.PollOptionRequest import io.getstream.chat.android.network.models.PollOptionResponse +import io.getstream.chat.android.network.models.PollResponse +import io.getstream.chat.android.network.models.PollVoteResponse +import io.getstream.chat.android.network.models.PollVotesResponse import io.getstream.chat.android.network.models.PushPreferenceInput import io.getstream.chat.android.network.models.PushPreferencesResponse import io.getstream.chat.android.network.models.QueryBannedUsersResponse @@ -154,6 +153,7 @@ import io.getstream.chat.android.network.models.QueryDraftsRequest import io.getstream.chat.android.network.models.QueryMembersPayload 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.QueryRemindersRequest import io.getstream.chat.android.network.models.QueryUsersResponse @@ -2925,7 +2925,7 @@ internal class MoshiChatApiTest { @ParameterizedTest @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#queryPollVotesInput") - fun testQueryPollVotes(call: RetrofitCall, expected: KClass<*>) = runTest { + fun testQueryPollVotes(call: RetrofitCall, expected: KClass<*>) = runTest { // given val api = mock() val pollId = randomString() 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 8bb391938e4e..4b6472803958 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 @@ -36,14 +36,10 @@ 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.ParsedPredefinedFilterResponse -import io.getstream.chat.android.client.api2.model.response.PollResponse -import io.getstream.chat.android.client.api2.model.response.PollVoteResponse import io.getstream.chat.android.client.api2.model.response.QueryChannelsResponse 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.QueryPollVotesResponse -import io.getstream.chat.android.client.api2.model.response.QueryPollsResponse 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 @@ -74,7 +70,11 @@ import io.getstream.chat.android.network.models.ListDevicesResponse import io.getstream.chat.android.network.models.ListUserGroupsResponse import io.getstream.chat.android.network.models.MembersResponse import io.getstream.chat.android.network.models.PollOptionResponse +import io.getstream.chat.android.network.models.PollResponse +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.QueryUsersResponse import io.getstream.chat.android.network.models.RemoveUserGroupMembersResponse import io.getstream.chat.android.network.models.Response @@ -660,11 +660,11 @@ internal object MoshiChatApiTestArguments { @JvmStatic fun queryPollVotesInput() = listOf( Arguments.of( - RetroSuccess(Mother.randomQueryPollVotesResponse()).toRetrofitCall(), + RetroSuccess(Mother.randomPollVotesResponse()).toRetrofitCall(), Result.Success::class, ), Arguments.of( - RetroError(statusCode = 500).toRetrofitCall(), + RetroError(statusCode = 500).toRetrofitCall(), Result.Failure::class, ), ) @@ -919,7 +919,7 @@ internal object MoshiChatApiTestArguments { Arguments.of( RetroSuccess( PollResponse( - poll = Mother.randomDownstreamPollDto(), + poll = Mother.randomPollResponseData(), duration = randomString(), ), ).toRetrofitCall(), @@ -946,7 +946,7 @@ internal object MoshiChatApiTestArguments { RetroSuccess( PollVoteResponse( duration = randomString(), - vote = Mother.randomDownstreamVoteDto(), + vote = Mother.randomPollVoteResponseData(), ), ).toRetrofitCall(), Result.Success::class, 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 121804fb5e35..2700348922a4 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 @@ -56,8 +56,8 @@ import io.getstream.chat.android.client.Mother.randomDownstreamUserGroupDto import io.getstream.chat.android.client.Mother.randomDownstreamVoteDto import io.getstream.chat.android.client.Mother.randomFileUploadConfig import io.getstream.chat.android.client.Mother.randomFullUserResponse +import io.getstream.chat.android.client.Mother.randomPollVotesResponse import io.getstream.chat.android.client.Mother.randomPrivacySettingsDto -import io.getstream.chat.android.client.Mother.randomQueryPollVotesResponse import io.getstream.chat.android.client.Mother.randomQueryPollsResponse import io.getstream.chat.android.client.Mother.randomQueryRemindersResponse import io.getstream.chat.android.client.Mother.randomRoleDto @@ -1509,8 +1509,8 @@ internal class DomainMappingTest { } @Test - fun `QueryPollVotesResponse is correctly mapped to QueryPollVotesResult`() { - val input = randomQueryPollVotesResponse() + fun `PollVotesResponse is correctly mapped to QueryPollVotesResult`() { + val input = randomPollVotesResponse() val sut = Fixture().get() val result = with(sut) { input.toDomain() } val expected = QueryPollVotesResult( diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/PollResponseParsingTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/PollResponseParsingTest.kt new file mode 100644 index 000000000000..a86fb4eb327c --- /dev/null +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/PollResponseParsingTest.kt @@ -0,0 +1,124 @@ +/* + * 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.client.api2.mapping.DomainMapping +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.PollResponse +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldHaveSize +import org.junit.jupiter.api.Test + +internal class PollResponseParsingTest { + private val parser = ParserFactory.createMoshiChatParser() + + private val domainMapping = DomainMapping( + currentUserIdProvider = { null }, + channelTransformer = NoOpChannelTransformer, + messageTransformer = NoOpMessageTransformer, + userTransformer = NoOpUserTransformer, + ) + + @Test + fun `Deserialize a poll response`() { + val poll = parser.fromJson(POLL_JSON, PollResponse::class.java).poll + + poll.id shouldBeEqualTo "pollId" + poll.name shouldBeEqualTo "Best Star Wars film" + poll.votingVisibility shouldBeEqualTo "public" + poll.voteCount shouldBeEqualTo 2 + poll.options shouldHaveSize 2 + } + + @Test + fun `Collect the root-level custom fields of a poll and its options`() { + val poll = parser.fromJson(POLL_JSON, PollResponse::class.java).poll + + poll.custom shouldBeEqualTo mapOf("category" to "cinema") + poll.options.first().custom shouldBeEqualTo mapOf("episode" to "V") + } + + @Test + fun `Option custom data survives the mapping to the domain`() { + val response = parser.fromJson(POLL_JSON, PollResponse::class.java).poll + + val poll = with(domainMapping) { response.toDomain() } + + // Collected into `custom` on the way in, and it has to reach `Option.extraData` too: the + // hand-written DTO path and the direct path both carry it. + poll.options.first().extraData shouldBeEqualTo mapOf("episode" to "V") + } + + @Test + fun `Deserialize the nested vote and its user`() { + val poll = parser.fromJson(POLL_JSON, PollResponse::class.java).poll + + poll.ownVotes shouldHaveSize 1 + poll.ownVotes.first().optionId shouldBeEqualTo "optionA" + poll.ownVotes.first().user?.id shouldBeEqualTo "leandro" + poll.ownVotes.first().user?.custom shouldBeEqualTo mapOf("birthland" to "Polis Massa") + } + + companion object { + private const val POLL_JSON = + """{ + "duration": "12ms", + "poll": { + "id": "pollId", + "name": "Best Star Wars film", + "description": "", + "created_by_id": "leandro", + "created_at": "2026-06-10T11:04:31.000Z", + "updated_at": "2026-06-10T11:04:31.000Z", + "allow_answers": false, + "allow_user_suggested_options": true, + "answers_count": 0, + "enforce_unique_vote": true, + "vote_count": 2, + "voting_visibility": "public", + "category": "cinema", + "options": [ + { "id": "optionA", "text": "The Empire Strikes Back", "episode": "V" }, + { "id": "optionB", "text": "A New Hope" } + ], + "vote_counts_by_option": { "optionA": 2 }, + "own_votes": [ + { + "id": "voteId", + "poll_id": "pollId", + "option_id": "optionA", + "created_at": "2026-06-10T11:04:31.000Z", + "updated_at": "2026-06-10T11:04:31.000Z", + "user_id": "leandro", + "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", + "birthland": "Polis Massa" + } + } + ] + } + }""" + } +} diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/PollTestData.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/PollTestData.kt index 18ce10e1c22f..4bb2d46aa8f2 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/PollTestData.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/PollTestData.kt @@ -29,7 +29,7 @@ internal object PollTestData { @Language("JSON") val jsonAllFields = - """{"id":"poll1","name":"Favorite color","description":"Choose your favorite color","options":[{"id":"option1","text":"Red"},{"id":"option2","text":"Blue"}],"voting_visibility":"public","enforce_unique_vote":true,"max_votes_allowed":1,"allow_user_suggested_options":false,"allow_answers":true,"vote_count":2,"vote_counts_by_option":{"option1":1,"option2":1},"latest_votes_by_option":{"option1":[{"id":"vote1","poll_id":"poll1","option_id":"option1","created_at":"2024-01-01T10:00:00.000Z","updated_at":"2024-01-01T11:00:00.000Z","user":{"id":"user1","role":"user","banned":false,"online":true},"is_answer":false}],"option2":[{"id":"vote2","poll_id":"poll1","option_id":"option2","created_at":"2024-01-01T10:00:00.000Z","updated_at":"2024-01-01T11:00:00.000Z","user":{"id":"user2","role":"user","banned":false,"online":true},"is_answer":false}]},"own_votes":[{"id":"vote1","poll_id":"poll1","option_id":"option1","created_at":"2024-01-01T10:00:00.000Z","updated_at":"2024-01-01T11:00:00.000Z","user":{"id":"user1","role":"user","banned":false,"online":true},"is_answer":false}],"created_at":"2024-01-01T09:00:00.000Z","updated_at":"2024-01-01T12:00:00.000Z","is_closed":false,"answers_count":1,"latest_answers":[{"id":"answer1","poll_id":"poll1","option_id":"","answer_text":"Purple","created_at":"2024-01-01T10:00:00.000Z","updated_at":"2024-01-01T11:00:00.000Z","user":{"id":"user3","role":"user","banned":false,"online":true},"is_answer":true}],"created_by":{"id":"admin","role":"admin","banned":false,"online":true},"created_by_id":"admin"}""" + """{"id":"poll1","name":"Favorite color","description":"Choose your favorite color","options":[{"id":"option1","text":"Red","shade":"crimson"},{"id":"option2","text":"Blue"}],"voting_visibility":"public","enforce_unique_vote":true,"max_votes_allowed":1,"allow_user_suggested_options":false,"allow_answers":true,"vote_count":2,"vote_counts_by_option":{"option1":1,"option2":1},"latest_votes_by_option":{"option1":[{"id":"vote1","poll_id":"poll1","option_id":"option1","created_at":"2024-01-01T10:00:00.000Z","updated_at":"2024-01-01T11:00:00.000Z","user":{"id":"user1","role":"user","banned":false,"online":true},"is_answer":false}],"option2":[{"id":"vote2","poll_id":"poll1","option_id":"option2","created_at":"2024-01-01T10:00:00.000Z","updated_at":"2024-01-01T11:00:00.000Z","user":{"id":"user2","role":"user","banned":false,"online":true},"is_answer":false}]},"own_votes":[{"id":"vote1","poll_id":"poll1","option_id":"option1","created_at":"2024-01-01T10:00:00.000Z","updated_at":"2024-01-01T11:00:00.000Z","user":{"id":"user1","role":"user","banned":false,"online":true},"is_answer":false}],"created_at":"2024-01-01T09:00:00.000Z","updated_at":"2024-01-01T12:00:00.000Z","is_closed":false,"answers_count":1,"latest_answers":[{"id":"answer1","poll_id":"poll1","option_id":"","answer_text":"Purple","created_at":"2024-01-01T10:00:00.000Z","updated_at":"2024-01-01T11:00:00.000Z","user":{"id":"user3","role":"user","banned":false,"online":true},"is_answer":true}],"created_by":{"id":"admin","role":"admin","banned":false,"online":true},"created_by_id":"admin"}""" @Language("JSON") val jsonOptionalFieldsMissing = @@ -68,7 +68,7 @@ internal object PollTestData { name = "Favorite color", description = "Choose your favorite color", options = listOf( - Option(id = "option1", text = "Red"), + Option(id = "option1", text = "Red", extraData = mapOf("shade" to "crimson")), Option(id = "option2", text = "Blue"), ), votingVisibility = VotingVisibility.PUBLIC,