diff --git a/CHANGELOG.md b/CHANGELOG.md index 4add7f4cfc9..ed03ec905a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ ### ⬆️ Improved ### ✅ Added +- Add `ChatClient::partialUpdateMember` for updating a `Member` in the scope of a `Channel`. [#5497](https://github.com/GetStream/stream-chat-android/pull/5497) +- Add `ChannelClient::partialUpdateMember` for updating a `Member` in the scope of a `Channel`. [#5497](https://github.com/GetStream/stream-chat-android/pull/5497) ### ⚠️ Changed diff --git a/stream-chat-android-client/api/stream-chat-android-client.api b/stream-chat-android-client/api/stream-chat-android-client.api index 7287ecea964..7255386a6cc 100644 --- a/stream-chat-android-client/api/stream-chat-android-client.api +++ b/stream-chat-android-client/api/stream-chat-android-client.api @@ -108,6 +108,8 @@ public final class io/getstream/chat/android/client/ChatClient { public final fun muteUser (Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun muteUser (Ljava/lang/String;Ljava/lang/Integer;)Lio/getstream/result/call/Call; public static synthetic fun muteUser$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)Lio/getstream/result/call/Call; + public final fun partialUpdateMember (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;)Lio/getstream/result/call/Call; + public static synthetic fun partialUpdateMember$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun partialUpdateMessage (Ljava/lang/String;Ljava/util/Map;Ljava/util/List;)Lio/getstream/result/call/Call; public static synthetic fun partialUpdateMessage$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun partialUpdateThread (Ljava/lang/String;Ljava/util/Map;Ljava/util/List;)Lio/getstream/result/call/Call; @@ -670,6 +672,8 @@ public final class io/getstream/chat/android/client/channel/ChannelClient { public final fun muteUser (Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun muteUser (Ljava/lang/String;Ljava/lang/Integer;)Lio/getstream/result/call/Call; public static synthetic fun muteUser$default (Lio/getstream/chat/android/client/channel/ChannelClient;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)Lio/getstream/result/call/Call; + public final fun partialUpdateMember (Ljava/lang/String;Ljava/util/Map;Ljava/util/List;)Lio/getstream/result/call/Call; + public static synthetic fun partialUpdateMember$default (Lio/getstream/chat/android/client/channel/ChannelClient;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun pinMessage (Lio/getstream/chat/android/models/Message;I)Lio/getstream/result/call/Call; public final fun pinMessage (Lio/getstream/chat/android/models/Message;Ljava/util/Date;)Lio/getstream/result/call/Call; public final fun query (Lio/getstream/chat/android/client/api/models/QueryChannelRequest;)Lio/getstream/result/call/Call; diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt index 4ec48817485..ea5d6641bfe 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt @@ -2350,6 +2350,34 @@ internal constructor( ) } + /** + * Updates specific fields of member data, retaining the custom data fields which were set previously. + * + * @param channelType The channel type. ie messaging. + * @param channelId The channel id. ie 123. + * @param userId The ID of the member to be updated. + * @param set The key-value data to be updated in the member data. + * @param unset The list of keys to be removed from the member data. + * + * @return Executable async [Call] responsible for updating member data. + */ + @CheckResult + public fun partialUpdateMember( + channelType: String, + channelId: String, + userId: String, + set: Map = emptyMap(), + unset: List = emptyList(), + ): Call { + return api.partialUpdateMember( + channelType = channelType, + channelId = channelId, + userId = userId, + set = set, + unset = unset, + ) + } + /** * Enables slow mode for the channel. When slow mode is enabled, users can only send a message every * [cooldownTimeInSeconds] time interval. The [cooldownTimeInSeconds] is specified in seconds, and should be diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt index 869e4c891c4..d351a871a06 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt @@ -371,6 +371,15 @@ internal interface ChatApi { skipPush: Boolean?, ): Call + @CheckResult + fun partialUpdateMember( + channelType: String, + channelId: String, + userId: String, + set: Map, + unset: List, + ): Call + @CheckResult fun queryMembers( channelType: String, diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/internal/ExtraDataValidator.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/internal/ExtraDataValidator.kt index e0d97d1883b..8ef28acd472 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/internal/ExtraDataValidator.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/internal/ExtraDataValidator.kt @@ -20,6 +20,7 @@ import io.getstream.chat.android.client.api.ChatApi import io.getstream.chat.android.client.api.ErrorCall import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.CustomObject +import io.getstream.chat.android.models.Member import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User import io.getstream.result.Error @@ -86,6 +87,18 @@ internal class ExtraDataValidator( .withExtraDataValidation(set) } + override fun partialUpdateMember( + channelType: String, + channelId: String, + userId: String, + set: Map, + unset: List, + ): Call { + return delegate + .partialUpdateMember(channelType, channelId, userId, set, unset) + .withExtraDataValidation(set) + } + private fun Call>.withExtraDataValidation( objects: List, ): Call> { @@ -144,6 +157,7 @@ internal class ExtraDataValidator( is Channel -> extraData.keys.filter(reservedInChannelPredicate) is Message -> extraData.keys.filter(reservedInChannelPredicate) is User -> extraData.keys.filter(reservedInChannelPredicate) + is Member -> extraData.keys.filter(reservedInMemberPredicate) else -> emptyList() } } @@ -153,6 +167,7 @@ internal class ExtraDataValidator( Channel::class -> keys.filter(reservedInChannelPredicate) Message::class -> keys.filter(reservedInMessagePredicate) User::class -> keys.filter(reservedInUserPredicate) + Member::class -> keys.filter(reservedInMemberPredicate) else -> emptyList() } } @@ -166,6 +181,7 @@ internal class ExtraDataValidator( is Channel -> "channel" is Message -> "message" is User -> "user" + is Member -> "member" else -> "" } } @@ -210,8 +226,24 @@ internal class ExtraDataValidator( "updated_at", ) + private val reservedInMember = setOf( + "user", + "created_at", + "updated_at", + "invited", + "invite_accepted_at", + "invite_rejected_at", + "shadow_banned", + "banned", + "channel_role", + "notifications_muted", + "status", + "ban_expires", + ) + private val reservedInChannelPredicate: (String) -> Boolean = reservedInChannel::contains private val reservedInMessagePredicate: (String) -> Boolean = reservedInMessage::contains private val reservedInUserPredicate: (String) -> Boolean = reservedInUser::contains + private val reservedInMemberPredicate: (String) -> Boolean = reservedInMember::contains } } 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 96d0f7a8500..6a515e8203e 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 @@ -78,6 +78,7 @@ import io.getstream.chat.android.client.api2.model.requests.UnblockUserRequest import io.getstream.chat.android.client.api2.model.requests.UpdateChannelPartialRequest import io.getstream.chat.android.client.api2.model.requests.UpdateChannelRequest import io.getstream.chat.android.client.api2.model.requests.UpdateCooldownRequest +import io.getstream.chat.android.client.api2.model.requests.UpdateMemberPartialRequest import io.getstream.chat.android.client.api2.model.requests.UpdateMessageRequest import io.getstream.chat.android.client.api2.model.requests.UpdateUsersRequest import io.getstream.chat.android.client.api2.model.requests.UpstreamOptionDto @@ -785,6 +786,23 @@ constructor( ).map(this::flattenChannel) } + override fun partialUpdateMember( + channelType: String, + channelId: String, + userId: String, + set: Map, + unset: List, + ): Call { + return channelApi.partialUpdateMember( + channelType = channelType, + channelId = channelId, + userId = userId, + body = UpdateMemberPartialRequest(set, unset), + ).map { response -> + response.channel_member.toDomain(currentUserIdProvider()) + } + } + private fun flattenChannel(response: ChannelResponse): Channel { return response.channel.toDomain(currentUserIdProvider(), null).let { channel -> channel.copy( diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/ChannelApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/ChannelApi.kt index 1b8a01555e7..378b1c7a08a 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/ChannelApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/ChannelApi.kt @@ -35,6 +35,8 @@ import io.getstream.chat.android.client.api2.model.requests.TruncateChannelReque import io.getstream.chat.android.client.api2.model.requests.UpdateChannelPartialRequest import io.getstream.chat.android.client.api2.model.requests.UpdateChannelRequest import io.getstream.chat.android.client.api2.model.requests.UpdateCooldownRequest +import io.getstream.chat.android.client.api2.model.requests.UpdateMemberPartialRequest +import io.getstream.chat.android.client.api2.model.requests.UpdateMemberPartialResponse import io.getstream.chat.android.client.api2.model.response.ChannelResponse import io.getstream.chat.android.client.api2.model.response.CompletableResponse import io.getstream.chat.android.client.api2.model.response.EventResponse @@ -135,6 +137,14 @@ internal interface ChannelApi { @Body body: InviteMembersRequest, ): RetrofitCall + @PATCH("/channels/{type}/{id}/member/{user_id}") + fun partialUpdateMember( + @Path("type") channelType: String, + @Path("id") channelId: String, + @Path("user_id") userId: String, + @Body body: UpdateMemberPartialRequest, + ): RetrofitCall + @POST("/channels/{type}/{id}/event") fun sendEvent( @Path("type") channelType: String, diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/MemberMapping.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/MemberMapping.kt index 9750db74a84..e524a353678 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/MemberMapping.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/MemberMapping.kt @@ -35,6 +35,7 @@ internal fun DownstreamMemberDto.toDomain(currentUserId: UserId?): Member = notificationsMuted = notifications_muted, status = status, banExpires = ban_expires, + extraData = extraData, ) internal fun Member.toDto(): UpstreamMemberDto = @@ -51,4 +52,5 @@ internal fun Member.toDto(): UpstreamMemberDto = notifications_muted = notificationsMuted, status = status, ban_expires = banExpires, + extraData = extraData, ) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/MemberDtos.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/MemberDtos.kt index 6cee0507c31..c3aba0c9b4c 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/MemberDtos.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/MemberDtos.kt @@ -17,8 +17,17 @@ package io.getstream.chat.android.client.api2.model.dto import com.squareup.moshi.JsonClass +import io.getstream.chat.android.core.internal.StreamHandsOff import java.util.Date +/** + * See [io.getstream.chat.android.client.parser2.adapters.UpstreamMemberDtoAdapter] for + * special [extraData] handling. + */ +@StreamHandsOff( + reason = "Field names can't be changed because [CustomObjectDtoAdapter] class uses reflections to add/remove " + + "content of [extraData] map", +) @JsonClass(generateAdapter = true) internal data class UpstreamMemberDto( val user: UpstreamUserDto, @@ -33,8 +42,18 @@ internal data class UpstreamMemberDto( val notifications_muted: Boolean?, val status: String?, val ban_expires: Date?, -) + val extraData: Map, +) : ExtraDataDto + +/** + * See [io.getstream.chat.android.client.parser2.adapters.DownstreamMemberDtoAdapter] for + * special [extraData] handling. + */ +@StreamHandsOff( + reason = "Field names can't be changed because [CustomObjectDtoAdapter] class uses reflections to add/remove " + + "content of [extraData] map", +) @JsonClass(generateAdapter = true) internal data class DownstreamMemberDto( val user: DownstreamUserDto, @@ -49,4 +68,6 @@ internal data class DownstreamMemberDto( val notifications_muted: Boolean?, val status: String?, val ban_expires: Date?, -) + + val extraData: Map, +) : ExtraDataDto diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/UpdateMemberPartialRequest.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/UpdateMemberPartialRequest.kt new file mode 100644 index 00000000000..bc50b605906 --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/UpdateMemberPartialRequest.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2014-2024 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.api2.model.requests + +import com.squareup.moshi.JsonClass + +/** + * Used to form a partial member update request. + * + * @param set Map of key-value pairs to set. + * @param unset List of keys to unset. + */ +@JsonClass(generateAdapter = true) +internal data class UpdateMemberPartialRequest( + val set: Map, + val unset: List, +) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/UpdateMemberPartialResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/UpdateMemberPartialResponse.kt new file mode 100644 index 00000000000..3475b1f3594 --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/UpdateMemberPartialResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2014-2024 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.api2.model.requests + +import com.squareup.moshi.JsonClass +import io.getstream.chat.android.client.api2.model.dto.DownstreamMemberDto + +/** + * Response model of the partial member update. + * + * @param channel_member The updated member object. + */ +@JsonClass(generateAdapter = true) +internal data class UpdateMemberPartialResponse( + val channel_member: DownstreamMemberDto, +) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt index 17198caa168..db2229299ba 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt @@ -625,6 +625,22 @@ public class ChannelClient internal constructor( return client.updateChannelPartial(channelType, channelId, set, unset) } + /** + * Updates specific fields of custom data for a given member. + * + * @param userId The user id of the member to update. + * @param set The key-value data to be updated in the member data. + * @param unset The list of keys to be removed from the member data. + */ + @CheckResult + public fun partialUpdateMember( + userId: String, + set: Map = emptyMap(), + unset: List = emptyList(), + ): Call { + return client.partialUpdateMember(channelType, channelId, userId, set, unset) + } + /** * Enables slow mode for the channel. When slow mode is enabled, users can only send a message every * [cooldownTimeInSeconds] time interval. The [cooldownTimeInSeconds] is specified in seconds, and should be 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 f4c7d20e55c..6ac01fa83c3 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 @@ -32,6 +32,7 @@ import io.getstream.chat.android.client.parser.ChatParser import io.getstream.chat.android.client.parser2.adapters.AttachmentDtoAdapter import io.getstream.chat.android.client.parser2.adapters.DateAdapter import io.getstream.chat.android.client.parser2.adapters.DownstreamChannelDtoAdapter +import io.getstream.chat.android.client.parser2.adapters.DownstreamMemberDtoAdapter import io.getstream.chat.android.client.parser2.adapters.DownstreamMessageDtoAdapter import io.getstream.chat.android.client.parser2.adapters.DownstreamModerationDetailsDtoAdapter import io.getstream.chat.android.client.parser2.adapters.DownstreamReactionDtoAdapter @@ -39,6 +40,7 @@ import io.getstream.chat.android.client.parser2.adapters.DownstreamUserDtoAdapte 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.UpstreamChannelDtoAdapter +import io.getstream.chat.android.client.parser2.adapters.UpstreamMemberDtoAdapter import io.getstream.chat.android.client.parser2.adapters.UpstreamMessageDtoAdapter import io.getstream.chat.android.client.parser2.adapters.UpstreamReactionDtoAdapter import io.getstream.chat.android.client.parser2.adapters.UpstreamUserDtoAdapter @@ -67,6 +69,8 @@ internal class MoshiChatParser( .add(UpstreamReactionDtoAdapter) .add(DownstreamUserDtoAdapter) .add(UpstreamUserDtoAdapter) + .add(DownstreamMemberDtoAdapter) + .add(UpstreamMemberDtoAdapter) .add(FlagRequestAdapterFactory) .build() } diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/MemberDtoAdapters.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/MemberDtoAdapters.kt new file mode 100644 index 00000000000..6b9224ba98a --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/MemberDtoAdapters.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2014-2024 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.client.api2.model.dto.DownstreamMemberDto +import io.getstream.chat.android.client.api2.model.dto.UpstreamMemberDto + +/** + * JSON adapter for [DownstreamMemberDto]. + * Handles the proper deserialization of the [extraData] field. + */ +internal object DownstreamMemberDtoAdapter : CustomObjectDtoAdapter(DownstreamMemberDto::class) { + + @FromJson + fun fromJson( + jsonReader: JsonReader, + mapAdapter: JsonAdapter>, + memberAdapter: JsonAdapter, + ): DownstreamMemberDto? = parseWithExtraData(jsonReader, mapAdapter, memberAdapter) + + @ToJson + @Suppress("UNUSED_PARAMETER") + fun toJson(jsonWriter: JsonWriter, value: DownstreamMemberDto): Unit = error("Can't convert this to Json") +} + +/** + * JSON adapter for [UpstreamMemberDto]. + * Handles the proper serialization of the [extraData] field. + */ +internal object UpstreamMemberDtoAdapter : CustomObjectDtoAdapter(UpstreamMemberDto::class) { + + @FromJson + @Suppress("UNUSED_PARAMETER") + fun fromJson(jsonReader: JsonReader): UpstreamMemberDto = error("Can't parse this from Json") + + @ToJson + fun toJson( + jsonWriter: JsonWriter, + member: UpstreamMemberDto?, + mapAdapter: JsonAdapter>, + memberAdapter: JsonAdapter, + ) = serializeWithExtraData(jsonWriter, member, mapAdapter, memberAdapter) +} diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/EventChatJsonProvider.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/EventChatJsonProvider.kt index f5f5b473267..2319365ec8c 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/EventChatJsonProvider.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/EventChatJsonProvider.kt @@ -633,7 +633,7 @@ private fun createMemberJsonString() = """ { "user": ${createUserJsonString()}, - "role": "user", + "channel_role": "channel_member", "created_at": "2020-06-29T06:14:28.000Z", "updated_at": "2020-06-29T06:14:28.000Z" } diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/chatclient/WhenPartialUpdateMember.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/chatclient/WhenPartialUpdateMember.kt new file mode 100644 index 00000000000..1565f2b1357 --- /dev/null +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/chatclient/WhenPartialUpdateMember.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2014-2024 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.chatclient + +import io.getstream.chat.android.client.ChatClient +import io.getstream.chat.android.models.Member +import io.getstream.chat.android.test.TestCall +import io.getstream.chat.android.test.callFrom +import io.getstream.result.Error +import io.getstream.result.Result +import io.getstream.result.call.Call +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeInstanceOf +import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +internal class WhenPartialUpdateMember : BaseChatClientTest() { + + @Test + fun `Given partialUpdateMember api call successful ChatClient should return success result`() = runTest { + val apiResult = callFrom { mock() } + val sut = Fixture().givenPartialUpdateMemberApiResult(apiResult).get() + + val result = sut.partialUpdateMember("channelType", "channelId", "userId").await() + + result shouldBeInstanceOf Result.Success::class + } + + @Test + fun `Given partialUpdateMember api call fails ChatClient should return error result`() = runTest { + val apiResult = TestCall(Result.Failure(Error.GenericError("Error"))) + val sut = Fixture().givenPartialUpdateMemberApiResult(apiResult).get() + + val result = sut.partialUpdateMember("channelType", "channelId", "userId").await() + + result shouldBeInstanceOf Result.Failure::class + } + + private inner class Fixture { + + fun givenPartialUpdateMemberApiResult(result: Call) = apply { + whenever(api.partialUpdateMember(any(), any(), any(), any(), any())) doReturn result + } + + fun get(): ChatClient = chatClient + } +} diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser/EventArguments.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser/EventArguments.kt index 424f6991975..8cd3605287c 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser/EventArguments.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser/EventArguments.kt @@ -151,6 +151,7 @@ internal object EventArguments { private val member = Member( user, + channelRole = "channel_member", createdAt = date, updatedAt = date, ) diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/DownstreamMemberDtoAdapterTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/DownstreamMemberDtoAdapterTest.kt new file mode 100644 index 00000000000..8aa36b9269b --- /dev/null +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/DownstreamMemberDtoAdapterTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2014-2024 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.model.dto.DownstreamMemberDto +import io.getstream.chat.android.client.parser2.testdata.MemberDtoTestData +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test + +internal class DownstreamMemberDtoAdapterTest { + + private val parser = MoshiChatParser { "" } + + @Test + fun `Deserialize JSON member with custom data`() { + val member = parser.fromJson(MemberDtoTestData.downstreamJsonWithExtraData, DownstreamMemberDto::class.java) + member shouldBeEqualTo MemberDtoTestData.downstreamMemberWithExtraData + } + + @Test + fun `Deserialize JSON member without custom data`() { + val member = parser.fromJson(MemberDtoTestData.downstreamJsonWithoutExtraData, DownstreamMemberDto::class.java) + member shouldBeEqualTo MemberDtoTestData.downstreamMemberWithoutExtraData + } +} diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/UpstreamMemberDtoAdapterTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/UpstreamMemberDtoAdapterTest.kt new file mode 100644 index 00000000000..18395a473bb --- /dev/null +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/UpstreamMemberDtoAdapterTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2014-2024 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.parser2.testdata.MemberDtoTestData +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test + +internal class UpstreamMemberDtoAdapterTest { + + private val parser = MoshiChatParser { "" } + + @Test + fun `Serialize JSON member with custom data`() { + val json = parser.toJson(MemberDtoTestData.upstreamMemberWithExtraData) + json shouldBeEqualTo MemberDtoTestData.upstreamJsonWithExtraData + } + + @Test + fun `Serialize JSON member without custom data`() { + val json = parser.toJson(MemberDtoTestData.upstreamMemberWithoutExtraData) + json shouldBeEqualTo MemberDtoTestData.upstreamJsonWithoutExtraData + } +} diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/ChannelDtoTestData.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/ChannelDtoTestData.kt index 9d23c43080f..56860ab353f 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/ChannelDtoTestData.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/ChannelDtoTestData.kt @@ -116,7 +116,6 @@ internal object ChannelDtoTestData { "members": [ { "user": ${UserDtoTestData.downstreamJson}, - "role": "owner", "created_at": "2020-06-10T11:04:31.0Z", "updated_at": "2020-06-10T11:04:31.588Z", "invited": true, @@ -147,7 +146,6 @@ internal object ChannelDtoTestData { "own_capabilities": ["connect-events", "pin-message"], "membership": { "user": ${UserDtoTestData.downstreamJson}, - "role": "owner", "created_at": "2020-06-10T11:04:31.0Z", "updated_at": "2020-06-10T11:04:31.588Z", "invited": true, @@ -188,6 +186,7 @@ internal object ChannelDtoTestData { notifications_muted = false, status = "member", ban_expires = Date(1615218151355), + extraData = emptyMap(), ), ), watchers = listOf(UserDtoTestData.downstreamUser), @@ -218,6 +217,7 @@ internal object ChannelDtoTestData { notifications_muted = false, status = "member", ban_expires = null, + extraData = emptyMap(), ), extraData = mapOf("draft" to true), ) @@ -373,6 +373,7 @@ internal object ChannelDtoTestData { notifications_muted = false, status = "member", ban_expires = Date(1615218151355), + extraData = emptyMap(), ), ), watchers = listOf(UserDtoTestData.upstreamUser), diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/MemberDtoTestData.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/MemberDtoTestData.kt new file mode 100644 index 00000000000..c11fe2283d8 --- /dev/null +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/MemberDtoTestData.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2014-2024 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.testdata + +import io.getstream.chat.android.client.api2.model.dto.DownstreamMemberDto +import io.getstream.chat.android.client.api2.model.dto.UpstreamMemberDto +import org.intellij.lang.annotations.Language + +internal object MemberDtoTestData { + + @Language("JSON") + val downstreamJsonWithExtraData = """ + { + "user" : ${UserDtoTestData.downstreamJson}, + "shadow_banned" : false, + "banned" : false, + "is_premium" : true + }""".withoutWhitespace() + + @Language("JSON") + val downstreamJsonWithoutExtraData = """ + { + "user" : ${UserDtoTestData.downstreamJson}, + "shadow_banned" : false, + "banned" : false + }""".withoutWhitespace() + + @Language("JSON") + val upstreamJsonWithExtraData = """ + { + "user" : ${UserDtoTestData.upstreamJson}, + "shadow_banned" : false, + "banned" : false, + "is_premium" : true + }""".withoutWhitespace() + + @Language("JSON") + val upstreamJsonWithoutExtraData = """ + { + "user" : ${UserDtoTestData.upstreamJson}, + "shadow_banned" : false, + "banned" : false + }""".withoutWhitespace() + + val downstreamMemberWithExtraData = DownstreamMemberDto( + user = UserDtoTestData.downstreamUser, + created_at = null, + updated_at = null, + invited = null, + invite_accepted_at = null, + invite_rejected_at = null, + shadow_banned = false, + banned = false, + channel_role = null, + notifications_muted = null, + status = null, + ban_expires = null, + extraData = mapOf("is_premium" to true), + ) + + val downstreamMemberWithoutExtraData = DownstreamMemberDto( + user = UserDtoTestData.downstreamUser, + created_at = null, + updated_at = null, + invited = null, + invite_accepted_at = null, + invite_rejected_at = null, + shadow_banned = false, + banned = false, + channel_role = null, + notifications_muted = null, + status = null, + ban_expires = null, + extraData = emptyMap(), + ) + + val upstreamMemberWithExtraData = UpstreamMemberDto( + user = UserDtoTestData.upstreamUser, + created_at = null, + updated_at = null, + invited = null, + invite_accepted_at = null, + invite_rejected_at = null, + shadow_banned = false, + banned = false, + channel_role = null, + notifications_muted = null, + status = null, + ban_expires = null, + extraData = mapOf("is_premium" to true), + ) + + val upstreamMemberWithoutExtraData = UpstreamMemberDto( + user = UserDtoTestData.upstreamUser, + created_at = null, + updated_at = null, + invited = null, + invite_accepted_at = null, + invite_rejected_at = null, + shadow_banned = false, + banned = false, + channel_role = null, + notifications_muted = null, + status = null, + ban_expires = null, + extraData = emptyMap(), + ) +} diff --git a/stream-chat-android-core/api/stream-chat-android-core.api b/stream-chat-android-core/api/stream-chat-android-core.api index cf90c925de9..9b733350aac 100644 --- a/stream-chat-android-core/api/stream-chat-android-core.api +++ b/stream-chat-android-core/api/stream-chat-android-core.api @@ -1036,13 +1036,14 @@ public final class io/getstream/chat/android/models/LinkPreview$Companion { public final fun getEMPTY ()Lio/getstream/chat/android/models/LinkPreview; } -public final class io/getstream/chat/android/models/Member : io/getstream/chat/android/models/UserEntity, io/getstream/chat/android/models/querysort/ComparableFieldProvider { - public fun (Lio/getstream/chat/android/models/User;Ljava/util/Date;Ljava/util/Date;Ljava/lang/Boolean;Ljava/util/Date;Ljava/util/Date;ZZLjava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/Date;)V - public synthetic fun (Lio/getstream/chat/android/models/User;Ljava/util/Date;Ljava/util/Date;Ljava/lang/Boolean;Ljava/util/Date;Ljava/util/Date;ZZLjava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/Date;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class io/getstream/chat/android/models/Member : io/getstream/chat/android/models/CustomObject, io/getstream/chat/android/models/UserEntity, io/getstream/chat/android/models/querysort/ComparableFieldProvider { + public fun (Lio/getstream/chat/android/models/User;Ljava/util/Date;Ljava/util/Date;Ljava/lang/Boolean;Ljava/util/Date;Ljava/util/Date;ZZLjava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/Date;Ljava/util/Map;)V + public synthetic fun (Lio/getstream/chat/android/models/User;Ljava/util/Date;Ljava/util/Date;Ljava/lang/Boolean;Ljava/util/Date;Ljava/util/Date;ZZLjava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/Date;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/models/User; public final fun component10 ()Ljava/lang/Boolean; public final fun component11 ()Ljava/lang/String; public final fun component12 ()Ljava/util/Date; + public final fun component13 ()Ljava/util/Map; public final fun component2 ()Ljava/util/Date; public final fun component3 ()Ljava/util/Date; public final fun component4 ()Ljava/lang/Boolean; @@ -1051,14 +1052,16 @@ public final class io/getstream/chat/android/models/Member : io/getstream/chat/a public final fun component7 ()Z public final fun component8 ()Z public final fun component9 ()Ljava/lang/String; - public final fun copy (Lio/getstream/chat/android/models/User;Ljava/util/Date;Ljava/util/Date;Ljava/lang/Boolean;Ljava/util/Date;Ljava/util/Date;ZZLjava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/Date;)Lio/getstream/chat/android/models/Member; - public static synthetic fun copy$default (Lio/getstream/chat/android/models/Member;Lio/getstream/chat/android/models/User;Ljava/util/Date;Ljava/util/Date;Ljava/lang/Boolean;Ljava/util/Date;Ljava/util/Date;ZZLjava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/Date;ILjava/lang/Object;)Lio/getstream/chat/android/models/Member; + public final fun copy (Lio/getstream/chat/android/models/User;Ljava/util/Date;Ljava/util/Date;Ljava/lang/Boolean;Ljava/util/Date;Ljava/util/Date;ZZLjava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/Date;Ljava/util/Map;)Lio/getstream/chat/android/models/Member; + public static synthetic fun copy$default (Lio/getstream/chat/android/models/Member;Lio/getstream/chat/android/models/User;Ljava/util/Date;Ljava/util/Date;Ljava/lang/Boolean;Ljava/util/Date;Ljava/util/Date;ZZLjava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/util/Date;Ljava/util/Map;ILjava/lang/Object;)Lio/getstream/chat/android/models/Member; public fun equals (Ljava/lang/Object;)Z public final fun getBanExpires ()Ljava/util/Date; public final fun getBanned ()Z public final fun getChannelRole ()Ljava/lang/String; public fun getComparableField (Ljava/lang/String;)Ljava/lang/Comparable; public final fun getCreatedAt ()Ljava/util/Date; + public fun getExtraData ()Ljava/util/Map; + public fun getExtraValue (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; public final fun getInviteAcceptedAt ()Ljava/util/Date; public final fun getInviteRejectedAt ()Ljava/util/Date; public final fun getNotificationsMuted ()Ljava/lang/Boolean; diff --git a/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Member.kt b/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Member.kt index 178acada904..334d537424e 100644 --- a/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Member.kt +++ b/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Member.kt @@ -81,7 +81,11 @@ public data class Member( * The date the ban expires. */ val banExpires: Date? = null, -) : UserEntity, ComparableFieldProvider { + /** + * A map of custom fields for the member. + */ + override val extraData: Map = emptyMap(), +) : UserEntity, CustomObject, ComparableFieldProvider { override fun getComparableField(fieldName: String): Comparable<*>? { return when (fieldName) { diff --git a/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/database/internal/ChatDatabase.kt b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/database/internal/ChatDatabase.kt index 73301f43c4c..16cc821f675 100644 --- a/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/database/internal/ChatDatabase.kt +++ b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/database/internal/ChatDatabase.kt @@ -81,7 +81,7 @@ import io.getstream.chat.android.offline.repository.domain.user.internal.UserEnt ThreadEntity::class, ThreadOrderEntity::class, ], - version = 79, + version = 80, exportSchema = false, ) @TypeConverters( diff --git a/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/channel/member/internal/MemberEntity.kt b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/channel/member/internal/MemberEntity.kt index 594d20cc553..e50f6c22c4d 100644 --- a/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/channel/member/internal/MemberEntity.kt +++ b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/channel/member/internal/MemberEntity.kt @@ -59,4 +59,7 @@ internal data class MemberEntity( /** The date the ban expires. */ var banExpires: Date? = null, + + /** Map of custom fields for the member. */ + val extraData: Map = emptyMap(), ) diff --git a/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/channel/member/internal/MemberMapper.kt b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/channel/member/internal/MemberMapper.kt index 1382ae5ddf7..86512e8fca2 100644 --- a/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/channel/member/internal/MemberMapper.kt +++ b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/channel/member/internal/MemberMapper.kt @@ -32,6 +32,7 @@ internal fun Member.toEntity(): MemberEntity = MemberEntity( notificationsMuted = notificationsMuted, status = status, banExpires = banExpires, + extraData = extraData, ) internal suspend fun MemberEntity.toModel(getUser: suspend (userId: String) -> User): Member = Member( @@ -47,4 +48,5 @@ internal suspend fun MemberEntity.toModel(getUser: suspend (userId: String) -> U notificationsMuted = notificationsMuted, status = status, banExpires = banExpires, + extraData = extraData, )