From 5d4e534e627c7c6606d1e56b8bd75a4ce4c91e9a Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:54:28 +0200 Subject: [PATCH 1/5] Add ChatClient user group endpoints Exposes the full /usergroups API surface: create, list (query), get, update, delete, search by name prefix, and add/remove members. Adds the matching DTOs and wires them through ChatApi/MoshiChatApi. --- .../api/stream-chat-android-client.api | 16 ++ .../chat/android/client/ChatClient.kt | 166 ++++++++++++++ .../chat/android/client/api/ChatApi.kt | 62 +++++ .../chat/android/client/api2/MoshiChatApi.kt | 114 ++++++++++ .../client/api2/endpoint/UserGroupApi.kt | 88 ++++++++ .../api2/model/requests/UserGroupRequests.kt | 48 ++++ .../api2/model/response/UserGroupResponses.kt | 32 +++ .../chat/android/client/di/ChatModule.kt | 2 + .../android/client/api2/MoshiChatApiTest.kt | 213 ++++++++++++++++++ .../client/api2/MoshiChatApiTestArguments.kt | 25 ++ 10 files changed, 766 insertions(+) create mode 100644 stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/UserGroupApi.kt create mode 100644 stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/UserGroupRequests.kt create mode 100644 stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/UserGroupResponses.kt 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 0ac3b95f1e3..26fb325498f 100644 --- a/stream-chat-android-client/api/stream-chat-android-client.api +++ b/stream-chat-android-client/api/stream-chat-android-client.api @@ -16,6 +16,8 @@ public final class io/getstream/chat/android/client/ChatClient { public final fun addMembers (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/models/Message;Ljava/lang/Boolean;Ljava/util/Date;Ljava/lang/Boolean;)Lio/getstream/result/call/Call; public static synthetic fun addMembers$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/models/Message;Ljava/lang/Boolean;Ljava/util/Date;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun addSocketListener (Lio/getstream/chat/android/client/socket/SocketListener;)V + public final fun addUserGroupMembers (Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;)Lio/getstream/result/call/Call; + public static synthetic fun addUserGroupMembers$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun appSettings ()Lio/getstream/result/call/Call; public final fun archiveChannel (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun banUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Lio/getstream/result/call/Call; @@ -44,6 +46,8 @@ public final class io/getstream/chat/android/client/ChatClient { public final fun createDraftMessage (Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/DraftMessage;)Lio/getstream/result/call/Call; public final fun createPollOption (Ljava/lang/String;Lio/getstream/chat/android/models/PollOption;)Lio/getstream/result/call/Call; public final fun createReminder (Ljava/lang/String;Ljava/util/Date;)Lio/getstream/result/call/Call; + public final fun createUserGroup (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lio/getstream/result/call/Call; + public static synthetic fun createUserGroup$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun deleteChannel (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun deleteDevice (Lio/getstream/chat/android/models/Device;)Lio/getstream/result/call/Call; public final fun deleteDraftMessages (Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/DraftMessage;)Lio/getstream/result/call/Call; @@ -60,6 +64,8 @@ public final class io/getstream/chat/android/client/ChatClient { public final fun deleteReaction (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public static synthetic fun deleteReaction$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun deleteReminder (Ljava/lang/String;)Lio/getstream/result/call/Call; + public final fun deleteUserGroup (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; + public static synthetic fun deleteUserGroup$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun devToken (Ljava/lang/String;)Ljava/lang/String; public final fun disableSlowMode (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun disconnect (Z)Lio/getstream/result/call/Call; @@ -103,6 +109,8 @@ public final class io/getstream/chat/android/client/ChatClient { public final fun getThread (Ljava/lang/String;Lio/getstream/chat/android/client/api/models/GetThreadOptions;)Lio/getstream/result/call/Call; public static synthetic fun getThread$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Lio/getstream/chat/android/client/api/models/GetThreadOptions;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun getUnreadCounts ()Lio/getstream/result/call/Call; + public final fun getUserGroup (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; + public static synthetic fun getUserGroup$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/call/Call; public static final fun getVERSION_PREFIX_HEADER ()Lio/getstream/chat/android/client/header/VersionPrefixHeader; public static final fun handlePushMessage (Lio/getstream/chat/android/models/PushMessage;)V public final fun hideChannel (Ljava/lang/String;Ljava/lang/String;Z)Lio/getstream/result/call/Call; @@ -170,6 +178,8 @@ public final class io/getstream/chat/android/client/ChatClient { public final fun queryReminders (Lio/getstream/chat/android/models/FilterObject;ILjava/lang/String;Lio/getstream/chat/android/models/querysort/QuerySorter;)Lio/getstream/result/call/Call; public static synthetic fun queryReminders$default (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/FilterObject;ILjava/lang/String;Lio/getstream/chat/android/models/querysort/QuerySorter;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun queryThreads (Lio/getstream/chat/android/client/api/models/QueryThreadsRequest;)Lio/getstream/result/call/Call; + public final fun queryUserGroups (Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; + public static synthetic fun queryUserGroups$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun queryUsers (Lio/getstream/chat/android/client/api/models/QueryUsersRequest;)Lio/getstream/result/call/Call; public final fun reconnectSocket ()Lio/getstream/result/call/Call; public final fun rejectInvite (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; @@ -178,8 +188,12 @@ public final class io/getstream/chat/android/client/ChatClient { public final fun removePollVote (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun removeShadowBan (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun removeSocketListener (Lio/getstream/chat/android/client/socket/SocketListener;)V + public final fun removeUserGroupMembers (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)Lio/getstream/result/call/Call; + public static synthetic fun removeUserGroupMembers$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun searchMessages (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/FilterObject;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Lio/getstream/chat/android/models/querysort/QuerySorter;)Lio/getstream/result/call/Call; public static synthetic fun searchMessages$default (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/FilterObject;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Lio/getstream/chat/android/models/querysort/QuerySorter;ILjava/lang/Object;)Lio/getstream/result/call/Call; + public final fun searchUserGroups (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; + public static synthetic fun searchUserGroups$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun sendAction (Lio/getstream/chat/android/client/api/models/SendActionRequest;)Lio/getstream/result/call/Call; public final fun sendEvent (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lio/getstream/result/call/Call; public static synthetic fun sendEvent$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lio/getstream/result/call/Call; @@ -251,6 +265,8 @@ public final class io/getstream/chat/android/client/ChatClient { public final fun updatePollOption (Ljava/lang/String;Lio/getstream/chat/android/models/PollOption;)Lio/getstream/result/call/Call; public final fun updateReminder (Ljava/lang/String;Ljava/util/Date;)Lio/getstream/result/call/Call; public final fun updateUser (Lio/getstream/chat/android/models/User;)Lio/getstream/result/call/Call; + public final fun updateUserGroup (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; + public static synthetic fun updateUserGroup$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun updateUsers (Ljava/util/List;)Lio/getstream/result/call/Call; public final fun uploadFile (Ljava/io/File;)Lio/getstream/result/call/Call; public final fun uploadFile (Ljava/io/File;Lio/getstream/chat/android/client/utils/ProgressCallback;)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 89b1f5cdc5b..94dc3d41a2a 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 @@ -220,6 +220,7 @@ import io.getstream.chat.android.models.UploadAttachmentsNetworkType import io.getstream.chat.android.models.UploadedFile import io.getstream.chat.android.models.User import io.getstream.chat.android.models.UserBlock +import io.getstream.chat.android.models.UserGroup import io.getstream.chat.android.models.Vote import io.getstream.chat.android.models.querysort.QuerySortByField import io.getstream.chat.android.models.querysort.QuerySorter @@ -1730,6 +1731,171 @@ internal constructor( } } + /** + * Creates a user group. + * + * @param name Display name (1–255 chars), unique within app/team. + * @param id Group ID (max 255 chars). A UUID v7 is generated server-side when null. + * @param description Optional description (max 1024 chars). + * @param teamId Team to scope the group to. Required when multi-tenancy is enabled. + * @param memberIds Initial member user IDs (max 100). + */ + @CheckResult + public fun createUserGroup( + name: String, + id: String? = null, + description: String? = null, + teamId: String? = null, + memberIds: List? = null, + ): Call { + return api.createUserGroup( + name = name, + id = id, + description = description, + teamId = teamId, + memberIds = memberIds, + ) + } + + /** + * Lists user groups, ordered for cursor pagination. + * + * @param limit Max groups to return (1–100). Server default applies when null. + * @param idGt Cursor: groups whose ID sorts after this value. + * @param createdAtGt Cursor: groups created after this RFC3339 timestamp. + * @param teamId Restrict to a specific team. + */ + @CheckResult + public fun queryUserGroups( + limit: Int? = null, + idGt: String? = null, + createdAtGt: String? = null, + teamId: String? = null, + ): Call> { + return api.queryUserGroups( + limit = limit, + idGt = idGt, + createdAtGt = createdAtGt, + teamId = teamId, + ) + } + + /** + * Searches user groups by name prefix. + * + * @param query Search term (1–255 chars). + * @param limit Maximum number of groups to return (1–25). Server default applies when null. + * @param teamId Restrict the search to a specific team. Required when multi-tenancy is enabled. + * @param nameGt Cursor: groups whose name sorts after this value. + * @param idGt Cursor: groups whose ID sorts after this value. + */ + @CheckResult + public fun searchUserGroups( + query: String, + limit: Int? = null, + teamId: String? = null, + nameGt: String? = null, + idGt: String? = null, + ): Call> { + return api.searchUserGroups( + query = query, + limit = limit, + teamId = teamId, + nameGt = nameGt, + idGt = idGt, + ) + } + + /** + * Fetches a user group by ID. + * + * @param teamId Required when multi-tenancy is enabled. + */ + @CheckResult + public fun getUserGroup( + id: String, + teamId: String? = null, + ): Call { + return api.getUserGroup(id = id, teamId = teamId) + } + + /** + * Updates a user group's metadata. Members are managed via [addUserGroupMembers] and + * [removeUserGroupMembers]. + * + * @param name New display name (1–255 chars). + * @param description New description (max 1024 chars). + * @param teamId Required when multi-tenancy is enabled. + */ + @CheckResult + public fun updateUserGroup( + id: String, + name: String? = null, + description: String? = null, + teamId: String? = null, + ): Call { + return api.updateUserGroup( + id = id, + name = name, + description = description, + teamId = teamId, + ) + } + + /** + * Deletes a user group. + * + * @param teamId Required when multi-tenancy is enabled. + */ + @CheckResult + public fun deleteUserGroup( + id: String, + teamId: String? = null, + ): Call { + return api.deleteUserGroup(id = id, teamId = teamId) + } + + /** + * Adds members to a user group. + * + * @param memberIds User IDs to add (1–100). + * @param asAdmin Whether to add the members as group admins. + * @param teamId Required when multi-tenancy is enabled. + */ + @CheckResult + public fun addUserGroupMembers( + id: String, + memberIds: List, + asAdmin: Boolean? = null, + teamId: String? = null, + ): Call { + return api.addUserGroupMembers( + id = id, + memberIds = memberIds, + asAdmin = asAdmin, + teamId = teamId, + ) + } + + /** + * Removes members from a user group. + * + * @param memberIds User IDs to remove (1–100). + * @param teamId Required when multi-tenancy is enabled. + */ + @CheckResult + public fun removeUserGroupMembers( + id: String, + memberIds: List, + teamId: String? = null, + ): Call { + return api.removeUserGroupMembers( + id = id, + memberIds = memberIds, + teamId = teamId, + ) + } + /** * Dismiss notifications from a given [channelType] and [channelId]. * Be sure to initialize ChatClient before calling this method! 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 634ecb7ab71..2747ebf36b2 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 @@ -65,6 +65,7 @@ import io.getstream.chat.android.models.UnreadCounts import io.getstream.chat.android.models.UploadedFile import io.getstream.chat.android.models.User import io.getstream.chat.android.models.UserBlock +import io.getstream.chat.android.models.UserGroup import io.getstream.chat.android.models.Vote import io.getstream.chat.android.models.querysort.QuerySorter import io.getstream.result.call.Call @@ -144,6 +145,67 @@ internal interface ChatApi { @CheckResult fun snoozeChannelPushNotifications(cid: String, until: Date): Call + @CheckResult + fun createUserGroup( + name: String, + id: String? = null, + description: String? = null, + teamId: String? = null, + memberIds: List? = null, + ): Call + + @CheckResult + fun queryUserGroups( + limit: Int? = null, + idGt: String? = null, + createdAtGt: String? = null, + teamId: String? = null, + ): Call> + + @CheckResult + fun searchUserGroups( + query: String, + limit: Int? = null, + teamId: String? = null, + nameGt: String? = null, + idGt: String? = null, + ): Call> + + @CheckResult + fun getUserGroup( + id: String, + teamId: String? = null, + ): Call + + @CheckResult + fun updateUserGroup( + id: String, + name: String? = null, + description: String? = null, + teamId: String? = null, + ): Call + + @CheckResult + fun deleteUserGroup( + id: String, + teamId: String? = null, + ): Call + + @CheckResult + fun addUserGroupMembers( + id: String, + memberIds: List, + asAdmin: Boolean? = null, + teamId: String? = null, + ): Call + + @CheckResult + fun removeUserGroupMembers( + id: String, + memberIds: List, + teamId: String? = null, + ): Call + @CheckResult fun searchMessages( channelFilter: FilterObject, 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 774e56025ac..5680d42ba62 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 @@ -41,6 +41,7 @@ import io.getstream.chat.android.client.api2.endpoint.PushPreferencesApi import io.getstream.chat.android.client.api2.endpoint.RemindersApi import io.getstream.chat.android.client.api2.endpoint.ThreadsApi import io.getstream.chat.android.client.api2.endpoint.UserApi +import io.getstream.chat.android.client.api2.endpoint.UserGroupApi import io.getstream.chat.android.client.api2.mapping.DomainMapping import io.getstream.chat.android.client.api2.mapping.DtoMapping import io.getstream.chat.android.client.api2.mapping.EventMapping @@ -50,9 +51,11 @@ import io.getstream.chat.android.client.api2.model.dto.UpstreamPushPreferenceInp import io.getstream.chat.android.client.api2.model.requests.AcceptInviteRequest import io.getstream.chat.android.client.api2.model.requests.AddDeviceRequest import io.getstream.chat.android.client.api2.model.requests.AddMembersRequest +import io.getstream.chat.android.client.api2.model.requests.AddUserGroupMembersRequest import io.getstream.chat.android.client.api2.model.requests.BanUserRequest import io.getstream.chat.android.client.api2.model.requests.BlockUserRequest import io.getstream.chat.android.client.api2.model.requests.CreatePollRequest +import io.getstream.chat.android.client.api2.model.requests.CreateUserGroupRequest import io.getstream.chat.android.client.api2.model.requests.FlagMessageRequest import io.getstream.chat.android.client.api2.model.requests.FlagRequest import io.getstream.chat.android.client.api2.model.requests.FlagUserRequest @@ -81,6 +84,7 @@ import io.getstream.chat.android.client.api2.model.requests.ReactionRequest import io.getstream.chat.android.client.api2.model.requests.RejectInviteRequest import io.getstream.chat.android.client.api2.model.requests.ReminderRequest import io.getstream.chat.android.client.api2.model.requests.RemoveMembersRequest +import io.getstream.chat.android.client.api2.model.requests.RemoveUserGroupMembersRequest import io.getstream.chat.android.client.api2.model.requests.SendActionRequest import io.getstream.chat.android.client.api2.model.requests.SendEventRequest import io.getstream.chat.android.client.api2.model.requests.SendMessageRequest @@ -93,6 +97,7 @@ import io.getstream.chat.android.client.api2.model.requests.UpdateCooldownReques import io.getstream.chat.android.client.api2.model.requests.UpdateLiveLocationRequest 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.UpdateUserGroupRequest import io.getstream.chat.android.client.api2.model.requests.UpdateUsersRequest import io.getstream.chat.android.client.api2.model.requests.UpsertPushPreferencesRequest import io.getstream.chat.android.client.api2.model.requests.UpstreamOptionDto @@ -100,6 +105,8 @@ import io.getstream.chat.android.client.api2.model.requests.UpstreamVoteDto import io.getstream.chat.android.client.api2.model.response.ChannelResponse import io.getstream.chat.android.client.api2.model.response.PushPreferencesResponse import io.getstream.chat.android.client.api2.model.response.TranslateMessageRequest +import io.getstream.chat.android.client.api2.model.response.UserGroupResponse +import io.getstream.chat.android.client.api2.model.response.UserGroupsResponse import io.getstream.chat.android.client.api2.model.response.getUserChannelPreference import io.getstream.chat.android.client.api2.model.response.getUserPreference import io.getstream.chat.android.client.call.RetrofitCall @@ -149,6 +156,7 @@ import io.getstream.chat.android.models.UnreadCounts import io.getstream.chat.android.models.UploadedFile import io.getstream.chat.android.models.User import io.getstream.chat.android.models.UserBlock +import io.getstream.chat.android.models.UserGroup import io.getstream.chat.android.models.Vote import io.getstream.chat.android.models.VotingVisibility import io.getstream.chat.android.models.querysort.QuerySorter @@ -191,6 +199,7 @@ constructor( private val pollsApi: PollsApi, private val remindersApi: RemindersApi, private val pushPreferencesApi: PushPreferencesApi, + private val userGroupApi: UserGroupApi, private val coroutineScope: CoroutineScope, private val userScope: UserScope, ) : ChatApi { @@ -519,6 +528,111 @@ constructor( .parseChannelPushPreferencesResponse(cid) } + override fun createUserGroup( + name: String, + id: String?, + description: String?, + teamId: String?, + memberIds: List?, + ): Call { + val body = CreateUserGroupRequest( + id = id, + name = name, + description = description, + team_id = teamId, + member_ids = memberIds, + ) + return userGroupApi.createUserGroup(body).mapUserGroup() + } + + override fun queryUserGroups( + limit: Int?, + idGt: String?, + createdAtGt: String?, + teamId: String?, + ): Call> { + return userGroupApi + .queryUserGroups( + limit = limit, + idGt = idGt, + createdAtGt = createdAtGt, + teamId = teamId, + ) + .mapUserGroups() + } + + override fun searchUserGroups( + query: String, + limit: Int?, + teamId: String?, + nameGt: String?, + idGt: String?, + ): Call> { + return userGroupApi + .searchUserGroups( + query = query, + limit = limit, + teamId = teamId, + nameGt = nameGt, + idGt = idGt, + ) + .mapUserGroups() + } + + override fun getUserGroup(id: String, teamId: String?): Call { + return userGroupApi.getUserGroup(id = id, teamId = teamId).mapUserGroup() + } + + override fun updateUserGroup( + id: String, + name: String?, + description: String?, + teamId: String?, + ): Call { + val body = UpdateUserGroupRequest( + name = name, + description = description, + team_id = teamId, + ) + return userGroupApi.updateUserGroup(id = id, body = body).mapUserGroup() + } + + override fun deleteUserGroup(id: String, teamId: String?): Call { + return userGroupApi.deleteUserGroup(id = id, teamId = teamId).toUnitCall() + } + + override fun addUserGroupMembers( + id: String, + memberIds: List, + asAdmin: Boolean?, + teamId: String?, + ): Call { + val body = AddUserGroupMembersRequest( + member_ids = memberIds, + as_admin = asAdmin, + team_id = teamId, + ) + return userGroupApi.addUserGroupMembers(id = id, body = body).mapUserGroup() + } + + override fun removeUserGroupMembers( + id: String, + memberIds: List, + teamId: String?, + ): Call { + val body = RemoveUserGroupMembersRequest( + member_ids = memberIds, + team_id = teamId, + ) + return userGroupApi.removeUserGroupMembers(id = id, body = body).mapUserGroup() + } + + private fun RetrofitCall.mapUserGroup() = + map { response -> with(domainMapping) { response.user_group.toDomain() } } + + private fun RetrofitCall.mapUserGroups() = + map { response -> with(domainMapping) { response.user_groups.map { it.toDomain() } } } + override fun muteCurrentUser(): Call { return muteUser( userId = userId, diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/UserGroupApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/UserGroupApi.kt new file mode 100644 index 00000000000..dad4ff84d6a --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/UserGroupApi.kt @@ -0,0 +1,88 @@ +/* + * 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.api2.endpoint + +import io.getstream.chat.android.client.api.AuthenticatedApi +import io.getstream.chat.android.client.api2.model.requests.AddUserGroupMembersRequest +import io.getstream.chat.android.client.api2.model.requests.CreateUserGroupRequest +import io.getstream.chat.android.client.api2.model.requests.RemoveUserGroupMembersRequest +import io.getstream.chat.android.client.api2.model.requests.UpdateUserGroupRequest +import io.getstream.chat.android.client.api2.model.response.CompletableResponse +import io.getstream.chat.android.client.api2.model.response.UserGroupResponse +import io.getstream.chat.android.client.api2.model.response.UserGroupsResponse +import io.getstream.chat.android.client.call.RetrofitCall +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +@AuthenticatedApi +internal interface UserGroupApi { + + @POST("/usergroups") + fun createUserGroup(@Body body: CreateUserGroupRequest): RetrofitCall + + @GET("/usergroups") + fun queryUserGroups( + @Query("limit") limit: Int? = null, + @Query("id_gt") idGt: String? = null, + @Query("created_at_gt") createdAtGt: String? = null, + @Query("team_id") teamId: String? = null, + ): RetrofitCall + + @GET("/usergroups/search") + fun searchUserGroups( + @Query("query") query: String, + @Query("limit") limit: Int? = null, + @Query("team_id") teamId: String? = null, + @Query("name_gt") nameGt: String? = null, + @Query("id_gt") idGt: String? = null, + ): RetrofitCall + + @GET("/usergroups/{id}") + fun getUserGroup( + @Path("id") id: String, + @Query("team_id") teamId: String? = null, + ): RetrofitCall + + @PUT("/usergroups/{id}") + fun updateUserGroup( + @Path("id") id: String, + @Body body: UpdateUserGroupRequest, + ): RetrofitCall + + @DELETE("/usergroups/{id}") + fun deleteUserGroup( + @Path("id") id: String, + @Query("team_id") teamId: String? = null, + ): RetrofitCall + + @POST("/usergroups/{id}/members") + fun addUserGroupMembers( + @Path("id") id: String, + @Body body: AddUserGroupMembersRequest, + ): RetrofitCall + + @POST("/usergroups/{id}/members/delete") + fun removeUserGroupMembers( + @Path("id") id: String, + @Body body: RemoveUserGroupMembersRequest, + ): RetrofitCall +} diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/UserGroupRequests.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/UserGroupRequests.kt new file mode 100644 index 00000000000..7ccf5313281 --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/UserGroupRequests.kt @@ -0,0 +1,48 @@ +/* + * 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.api2.model.requests + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class CreateUserGroupRequest( + val id: String? = null, + val name: String, + val description: String? = null, + val team_id: String? = null, + val member_ids: List? = null, +) + +@JsonClass(generateAdapter = true) +internal data class UpdateUserGroupRequest( + val name: String? = null, + val description: String? = null, + val team_id: String? = null, +) + +@JsonClass(generateAdapter = true) +internal data class AddUserGroupMembersRequest( + val member_ids: List, + val as_admin: Boolean? = null, + val team_id: String? = null, +) + +@JsonClass(generateAdapter = true) +internal data class RemoveUserGroupMembersRequest( + val member_ids: List, + val team_id: String? = null, +) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/UserGroupResponses.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/UserGroupResponses.kt new file mode 100644 index 00000000000..37aa2920262 --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/UserGroupResponses.kt @@ -0,0 +1,32 @@ +/* + * 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.api2.model.response + +import com.squareup.moshi.JsonClass +import io.getstream.chat.android.client.api2.model.dto.DownstreamUserGroupDto + +@JsonClass(generateAdapter = true) +internal data class UserGroupResponse( + val user_group: DownstreamUserGroupDto, + val duration: String? = null, +) + +@JsonClass(generateAdapter = true) +internal data class UserGroupsResponse( + val user_groups: List = emptyList(), + val duration: String? = null, +) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt index 4130e633d61..53b6cf3e992 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt @@ -53,6 +53,7 @@ import io.getstream.chat.android.client.api2.endpoint.PushPreferencesApi import io.getstream.chat.android.client.api2.endpoint.RemindersApi import io.getstream.chat.android.client.api2.endpoint.ThreadsApi import io.getstream.chat.android.client.api2.endpoint.UserApi +import io.getstream.chat.android.client.api2.endpoint.UserGroupApi import io.getstream.chat.android.client.api2.mapping.DomainMapping import io.getstream.chat.android.client.api2.mapping.DtoMapping import io.getstream.chat.android.client.api2.mapping.EventMapping @@ -352,6 +353,7 @@ constructor( buildRetrofitApi(), buildRetrofitApi(), buildRetrofitApi(), + buildRetrofitApi(), userScope, userScope, ).let { originalApi -> 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 744451a64b2..98d4fc7bf80 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 @@ -33,6 +33,7 @@ import io.getstream.chat.android.client.api2.endpoint.PushPreferencesApi import io.getstream.chat.android.client.api2.endpoint.RemindersApi import io.getstream.chat.android.client.api2.endpoint.ThreadsApi import io.getstream.chat.android.client.api2.endpoint.UserApi +import io.getstream.chat.android.client.api2.endpoint.UserGroupApi import io.getstream.chat.android.client.api2.mapping.DomainMapping import io.getstream.chat.android.client.api2.mapping.DtoMapping import io.getstream.chat.android.client.api2.mapping.EventMapping @@ -44,9 +45,11 @@ import io.getstream.chat.android.client.api2.model.dto.UnreadDto import io.getstream.chat.android.client.api2.model.dto.UpstreamPushPreferenceInputDto import io.getstream.chat.android.client.api2.model.requests.AcceptInviteRequest import io.getstream.chat.android.client.api2.model.requests.AddDeviceRequest +import io.getstream.chat.android.client.api2.model.requests.AddUserGroupMembersRequest import io.getstream.chat.android.client.api2.model.requests.BanUserRequest import io.getstream.chat.android.client.api2.model.requests.BlockUserRequest import io.getstream.chat.android.client.api2.model.requests.CreatePollRequest +import io.getstream.chat.android.client.api2.model.requests.CreateUserGroupRequest import io.getstream.chat.android.client.api2.model.requests.DeliveredMessageDto import io.getstream.chat.android.client.api2.model.requests.FlagMessageRequest import io.getstream.chat.android.client.api2.model.requests.FlagUserRequest @@ -70,6 +73,7 @@ import io.getstream.chat.android.client.api2.model.requests.QueryReactionsReques import io.getstream.chat.android.client.api2.model.requests.QueryRemindersRequest import io.getstream.chat.android.client.api2.model.requests.RejectInviteRequest import io.getstream.chat.android.client.api2.model.requests.ReminderRequest +import io.getstream.chat.android.client.api2.model.requests.RemoveUserGroupMembersRequest import io.getstream.chat.android.client.api2.model.requests.SendActionRequest import io.getstream.chat.android.client.api2.model.requests.SendEventRequest import io.getstream.chat.android.client.api2.model.requests.UnblockUserRequest @@ -78,6 +82,7 @@ import io.getstream.chat.android.client.api2.model.requests.UpdateCooldownReques import io.getstream.chat.android.client.api2.model.requests.UpdateLiveLocationRequest 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.requests.UpdateUserGroupRequest import io.getstream.chat.android.client.api2.model.requests.UpsertPushPreferencesRequest import io.getstream.chat.android.client.api2.model.requests.UpstreamOptionDto import io.getstream.chat.android.client.api2.model.requests.UpstreamVoteDto @@ -118,6 +123,8 @@ import io.getstream.chat.android.client.api2.model.response.TokenResponse import io.getstream.chat.android.client.api2.model.response.TranslateMessageRequest import io.getstream.chat.android.client.api2.model.response.UnblockUserResponse import io.getstream.chat.android.client.api2.model.response.UpdateUsersResponse +import io.getstream.chat.android.client.api2.model.response.UserGroupResponse +import io.getstream.chat.android.client.api2.model.response.UserGroupsResponse import io.getstream.chat.android.client.api2.model.response.UsersResponse import io.getstream.chat.android.client.call.RetrofitCall import io.getstream.chat.android.client.parser.toMap @@ -2924,6 +2931,206 @@ internal class MoshiChatApiTest { verify(api, times(1)).warmUp() } + @ParameterizedTest + @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#userGroupResponseInput") + fun testCreateUserGroup(call: RetrofitCall, expected: KClass<*>) = runTest { + val api = mock() + whenever(api.createUserGroup(any())).doReturn(call) + val sut = Fixture().withUserGroupApi(api).get() + + val name = randomString() + val id = randomString() + val description = randomString() + val teamId = randomString() + val memberIds = listOf(randomString(), randomString()) + val result = sut.createUserGroup( + name = name, + id = id, + description = description, + teamId = teamId, + memberIds = memberIds, + ).await() + + result `should be instance of` expected + verify(api, times(1)).createUserGroup( + CreateUserGroupRequest( + id = id, + name = name, + description = description, + team_id = teamId, + member_ids = memberIds, + ), + ) + } + + @ParameterizedTest + @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#userGroupsResponseInput") + fun testQueryUserGroups(call: RetrofitCall, expected: KClass<*>) = runTest { + val api = mock() + whenever( + api.queryUserGroups( + limit = anyOrNull(), + idGt = anyOrNull(), + createdAtGt = anyOrNull(), + teamId = anyOrNull(), + ), + ).doReturn(call) + val sut = Fixture().withUserGroupApi(api).get() + + val limit = randomInt() + val idGt = randomString() + val createdAtGt = randomString() + val teamId = randomString() + val result = sut.queryUserGroups( + limit = limit, + idGt = idGt, + createdAtGt = createdAtGt, + teamId = teamId, + ).await() + + result `should be instance of` expected + verify(api, times(1)).queryUserGroups( + limit = limit, + idGt = idGt, + createdAtGt = createdAtGt, + teamId = teamId, + ) + } + + @ParameterizedTest + @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#userGroupsResponseInput") + fun testSearchUserGroups(call: RetrofitCall, expected: KClass<*>) = runTest { + // given + val api = mock() + whenever( + api.searchUserGroups( + query = any(), + limit = anyOrNull(), + teamId = anyOrNull(), + nameGt = anyOrNull(), + idGt = anyOrNull(), + ), + ).doReturn(call) + val sut = Fixture() + .withUserGroupApi(api) + .get() + // when + val query = randomString() + val limit = randomInt() + val teamId = randomString() + val nameGt = randomString() + val idGt = randomString() + val result = sut.searchUserGroups(query, limit, teamId, nameGt, idGt).await() + // then + result `should be instance of` expected + verify(api, times(1)).searchUserGroups( + query = query, + limit = limit, + teamId = teamId, + nameGt = nameGt, + idGt = idGt, + ) + } + + @ParameterizedTest + @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#userGroupResponseInput") + fun testGetUserGroup(call: RetrofitCall, expected: KClass<*>) = runTest { + val api = mock() + whenever(api.getUserGroup(id = any(), teamId = anyOrNull())).doReturn(call) + val sut = Fixture().withUserGroupApi(api).get() + + val id = randomString() + val teamId = randomString() + val result = sut.getUserGroup(id = id, teamId = teamId).await() + + result `should be instance of` expected + verify(api, times(1)).getUserGroup(id = id, teamId = teamId) + } + + @ParameterizedTest + @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#userGroupResponseInput") + fun testUpdateUserGroup(call: RetrofitCall, expected: KClass<*>) = runTest { + val api = mock() + whenever(api.updateUserGroup(id = any(), body = any())).doReturn(call) + val sut = Fixture().withUserGroupApi(api).get() + + val id = randomString() + val name = randomString() + val description = randomString() + val teamId = randomString() + val result = sut.updateUserGroup( + id = id, + name = name, + description = description, + teamId = teamId, + ).await() + + result `should be instance of` expected + verify(api, times(1)).updateUserGroup( + id = id, + body = UpdateUserGroupRequest(name = name, description = description, team_id = teamId), + ) + } + + @ParameterizedTest + @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#deleteUserGroupInput") + fun testDeleteUserGroup(call: RetrofitCall, expected: KClass<*>) = runTest { + val api = mock() + whenever(api.deleteUserGroup(id = any(), teamId = anyOrNull())).doReturn(call) + val sut = Fixture().withUserGroupApi(api).get() + + val id = randomString() + val teamId = randomString() + val result = sut.deleteUserGroup(id = id, teamId = teamId).await() + + result `should be instance of` expected + verify(api, times(1)).deleteUserGroup(id = id, teamId = teamId) + } + + @ParameterizedTest + @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#userGroupResponseInput") + fun testAddUserGroupMembers(call: RetrofitCall, expected: KClass<*>) = runTest { + val api = mock() + whenever(api.addUserGroupMembers(id = any(), body = any())).doReturn(call) + val sut = Fixture().withUserGroupApi(api).get() + + val id = randomString() + val memberIds = listOf(randomString(), randomString()) + val asAdmin = randomBoolean() + val teamId = randomString() + val result = sut.addUserGroupMembers( + id = id, + memberIds = memberIds, + asAdmin = asAdmin, + teamId = teamId, + ).await() + + result `should be instance of` expected + verify(api, times(1)).addUserGroupMembers( + id = id, + body = AddUserGroupMembersRequest(member_ids = memberIds, as_admin = asAdmin, team_id = teamId), + ) + } + + @ParameterizedTest + @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#userGroupResponseInput") + fun testRemoveUserGroupMembers(call: RetrofitCall, expected: KClass<*>) = runTest { + val api = mock() + whenever(api.removeUserGroupMembers(id = any(), body = any())).doReturn(call) + val sut = Fixture().withUserGroupApi(api).get() + + val id = randomString() + val memberIds = listOf(randomString(), randomString()) + val teamId = randomString() + val result = sut.removeUserGroupMembers(id = id, memberIds = memberIds, teamId = teamId).await() + + result `should be instance of` expected + verify(api, times(1)).removeUserGroupMembers( + id = id, + body = RemoveUserGroupMembersRequest(member_ids = memberIds, team_id = teamId), + ) + } + private class Fixture { private var currentUserId: String = "" @@ -2945,6 +3152,7 @@ internal class MoshiChatApiTest { private var pollsApi: PollsApi = mock() private var remindersApi: RemindersApi = mock() private var pushPreferencesApi: PushPreferencesApi = mock() + private var userGroupApi: UserGroupApi = mock() private var fileUploader: FileUploader = mock() private var fileTransformer: FileTransformer = NoOpFileTransformer @@ -3009,6 +3217,10 @@ internal class MoshiChatApiTest { this.pushPreferencesApi = pushPreferencesApi } + fun withUserGroupApi(userGroupApi: UserGroupApi) = apply { + this.userGroupApi = userGroupApi + } + fun withFileUploader(fileUploader: FileUploader) = apply { this.fileUploader = fileUploader } @@ -3044,6 +3256,7 @@ internal class MoshiChatApiTest { pollsApi = pollsApi, remindersApi = remindersApi, pushPreferencesApi = pushPreferencesApi, + userGroupApi = userGroupApi, userScope = UserScope(ClientScope()), coroutineScope = testCoroutineExtension.scope, ) 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 ba0b16da70a..586d2a13aa2 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 @@ -59,6 +59,8 @@ import io.getstream.chat.android.client.api2.model.response.ReactionResponse import io.getstream.chat.android.client.api2.model.response.ReactionsResponse import io.getstream.chat.android.client.api2.model.response.ReminderResponse import io.getstream.chat.android.client.api2.model.response.SearchMessagesResponse +import io.getstream.chat.android.client.api2.model.response.UserGroupResponse +import io.getstream.chat.android.client.api2.model.response.UserGroupsResponse import io.getstream.chat.android.client.api2.model.response.SyncHistoryResponse import io.getstream.chat.android.client.api2.model.response.ThreadInfoResponse import io.getstream.chat.android.client.api2.model.response.ThreadResponse @@ -881,4 +883,27 @@ internal object MoshiChatApiTestArguments { ), Arguments.of(RetroError(statusCode = 500).toRetrofitCall(), Result.Failure::class), ) + + @JvmStatic + fun userGroupsResponseInput() = listOf( + Arguments.of( + RetroSuccess( + UserGroupsResponse(user_groups = listOf(Mother.randomDownstreamUserGroupDto())), + ).toRetrofitCall(), + Result.Success::class, + ), + Arguments.of(RetroError(statusCode = 500).toRetrofitCall(), Result.Failure::class), + ) + + @JvmStatic + fun userGroupResponseInput() = listOf( + Arguments.of( + RetroSuccess(UserGroupResponse(user_group = Mother.randomDownstreamUserGroupDto())).toRetrofitCall(), + Result.Success::class, + ), + Arguments.of(RetroError(statusCode = 500).toRetrofitCall(), Result.Failure::class), + ) + + @JvmStatic + fun deleteUserGroupInput() = completableResponseArguments() } From 8a2888d75fdee69dd4a364e2d6849e16de383f30 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Thu, 4 Jun 2026 18:06:29 +0200 Subject: [PATCH 2/5] Add ChatClient.searchRoles Exposes a new public ChatClient function backed by the `/roles/search` endpoint. Adds the `Role` domain model and wires the response path through the Moshi API layer. --- .../api/stream-chat-android-client.api | 2 + .../chat/android/client/ChatClient.kt | 29 +++++++++++++ .../chat/android/client/api/ChatApi.kt | 10 +++++ .../chat/android/client/api2/MoshiChatApi.kt | 23 ++++++++++ .../android/client/api2/endpoint/RoleApi.kt | 36 ++++++++++++++++ .../client/api2/mapping/DomainMapping.kt | 10 +++++ .../api2/model/dto/DownstreamRoleDto.kt | 29 +++++++++++++ .../model/response/SearchRolesResponse.kt | 26 +++++++++++ .../chat/android/client/di/ChatModule.kt | 2 + .../getstream/chat/android/client/Mother.kt | 15 +++++++ .../android/client/api2/MoshiChatApiTest.kt | 43 +++++++++++++++++++ .../client/api2/MoshiChatApiTestArguments.kt | 12 ++++++ .../api/stream-chat-android-core.api | 20 +++++++++ .../io/getstream/chat/android/models/Role.kt | 38 ++++++++++++++++ 14 files changed, 295 insertions(+) create mode 100644 stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/RoleApi.kt create mode 100644 stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/DownstreamRoleDto.kt create mode 100644 stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/SearchRolesResponse.kt create mode 100644 stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Role.kt 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 26fb325498f..d26c15f9110 100644 --- a/stream-chat-android-client/api/stream-chat-android-client.api +++ b/stream-chat-android-client/api/stream-chat-android-client.api @@ -192,6 +192,8 @@ public final class io/getstream/chat/android/client/ChatClient { public static synthetic fun removeUserGroupMembers$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun searchMessages (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/FilterObject;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Lio/getstream/chat/android/models/querysort/QuerySorter;)Lio/getstream/result/call/Call; public static synthetic fun searchMessages$default (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/FilterObject;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Lio/getstream/chat/android/models/querysort/QuerySorter;ILjava/lang/Object;)Lio/getstream/result/call/Call; + public final fun searchRoles (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)Lio/getstream/result/call/Call; + public static synthetic fun searchRoles$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun searchUserGroups (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public static synthetic fun searchUserGroups$default (Lio/getstream/chat/android/client/ChatClient;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun sendAction (Lio/getstream/chat/android/client/api/models/SendActionRequest;)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 94dc3d41a2a..e86a2e498ea 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 @@ -212,6 +212,7 @@ import io.getstream.chat.android.models.QueryReactionsResult import io.getstream.chat.android.models.QueryRemindersResult import io.getstream.chat.android.models.QueryThreadsResult import io.getstream.chat.android.models.Reaction +import io.getstream.chat.android.models.Role import io.getstream.chat.android.models.SearchMessagesResult import io.getstream.chat.android.models.Thread import io.getstream.chat.android.models.ThreadInfo @@ -1896,6 +1897,34 @@ internal constructor( ) } + /** + * Searches roles by name prefix. + * + * @param query Case-insensitive name prefix (1-255 chars). + * @param limit Max number of roles to return (1-25). Server default applies when null. + * @param roleType Optional filter: `"user"` for user-assignable roles, `"channel"` for + * channel-assignable roles. Both kinds are returned when null. + * @param includeGlobalRoles When `true`, include cross-team operator roles whose name starts + * with `global_`. Defaults to `false` server-side. + * @param nameGt Cursor: roles whose name sorts after this value. + */ + @CheckResult + public fun searchRoles( + query: String, + limit: Int? = null, + roleType: String? = null, + includeGlobalRoles: Boolean? = null, + nameGt: String? = null, + ): Call> { + return api.searchRoles( + query = query, + limit = limit, + roleType = roleType, + includeGlobalRoles = includeGlobalRoles, + nameGt = nameGt, + ) + } + /** * Dismiss notifications from a given [channelType] and [channelId]. * Be sure to initialize ChatClient before calling this method! 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 2747ebf36b2..a648fd368b4 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 @@ -58,6 +58,7 @@ import io.getstream.chat.android.models.QueryReactionsResult import io.getstream.chat.android.models.QueryRemindersResult import io.getstream.chat.android.models.QueryThreadsResult import io.getstream.chat.android.models.Reaction +import io.getstream.chat.android.models.Role import io.getstream.chat.android.models.SearchMessagesResult import io.getstream.chat.android.models.Thread import io.getstream.chat.android.models.ThreadInfo @@ -206,6 +207,15 @@ internal interface ChatApi { teamId: String? = null, ): Call + @CheckResult + fun searchRoles( + query: String, + limit: Int? = null, + roleType: String? = null, + includeGlobalRoles: Boolean? = null, + nameGt: String? = null, + ): Call> + @CheckResult fun searchMessages( channelFilter: FilterObject, 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 5680d42ba62..6e631ac3b5b 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 @@ -39,6 +39,7 @@ import io.getstream.chat.android.client.api2.endpoint.OpenGraphApi import io.getstream.chat.android.client.api2.endpoint.PollsApi import io.getstream.chat.android.client.api2.endpoint.PushPreferencesApi import io.getstream.chat.android.client.api2.endpoint.RemindersApi +import io.getstream.chat.android.client.api2.endpoint.RoleApi import io.getstream.chat.android.client.api2.endpoint.ThreadsApi import io.getstream.chat.android.client.api2.endpoint.UserApi import io.getstream.chat.android.client.api2.endpoint.UserGroupApi @@ -149,6 +150,7 @@ import io.getstream.chat.android.models.QueryReactionsResult import io.getstream.chat.android.models.QueryRemindersResult import io.getstream.chat.android.models.QueryThreadsResult import io.getstream.chat.android.models.Reaction +import io.getstream.chat.android.models.Role import io.getstream.chat.android.models.SearchMessagesResult import io.getstream.chat.android.models.Thread import io.getstream.chat.android.models.ThreadInfo @@ -200,6 +202,7 @@ constructor( private val remindersApi: RemindersApi, private val pushPreferencesApi: PushPreferencesApi, private val userGroupApi: UserGroupApi, + private val roleApi: RoleApi, private val coroutineScope: CoroutineScope, private val userScope: UserScope, ) : ChatApi { @@ -633,6 +636,26 @@ constructor( private fun RetrofitCall.mapUserGroups() = map { response -> with(domainMapping) { response.user_groups.map { it.toDomain() } } } + override fun searchRoles( + query: String, + limit: Int?, + roleType: String?, + includeGlobalRoles: Boolean?, + nameGt: String?, + ): Call> { + return roleApi + .searchRoles( + query = query, + limit = limit, + roleType = roleType, + includeGlobalRoles = includeGlobalRoles, + nameGt = nameGt, + ) + .map { response -> + with(domainMapping) { response.roles.map { it.toDomain() } } + } + } + override fun muteCurrentUser(): Call { return muteUser( userId = userId, diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/RoleApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/RoleApi.kt new file mode 100644 index 00000000000..63b2eeaa5eb --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/RoleApi.kt @@ -0,0 +1,36 @@ +/* + * 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.api2.endpoint + +import io.getstream.chat.android.client.api.AuthenticatedApi +import io.getstream.chat.android.client.api2.model.response.SearchRolesResponse +import io.getstream.chat.android.client.call.RetrofitCall +import retrofit2.http.GET +import retrofit2.http.Query + +@AuthenticatedApi +internal interface RoleApi { + + @GET("/roles/search") + fun searchRoles( + @Query("query") query: String, + @Query("limit") limit: Int? = null, + @Query("role_type") roleType: String? = null, + @Query("include_global_roles") includeGlobalRoles: Boolean? = null, + @Query("name_gt") nameGt: String? = null, + ): RetrofitCall +} 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 856653e7ba2..e5c162b7276 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 @@ -45,6 +45,7 @@ import io.getstream.chat.android.client.api2.model.dto.DownstreamReactionDto import io.getstream.chat.android.client.api2.model.dto.DownstreamReactionGroupDto 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.DownstreamRoleDto 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 @@ -113,6 +114,7 @@ import io.getstream.chat.android.models.QueryPollsResult import io.getstream.chat.android.models.QueryRemindersResult import io.getstream.chat.android.models.Reaction import io.getstream.chat.android.models.ReactionGroup +import io.getstream.chat.android.models.Role import io.getstream.chat.android.models.SearchWarning import io.getstream.chat.android.models.Thread import io.getstream.chat.android.models.ThreadInfo @@ -957,6 +959,14 @@ internal class DomainMapping( createdAt = created_at, ) + internal fun DownstreamRoleDto.toDomain(): Role = Role( + name = name, + custom = custom, + scopes = scopes, + createdAt = created_at, + updatedAt = updated_at, + ) + private companion object { private const val FIELD_LAST_MESSAGE_AT = "last_message_at" private const val FIELD_LAST_UPDATED = "last_updated" diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/DownstreamRoleDto.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/DownstreamRoleDto.kt new file mode 100644 index 00000000000..6bb362470e2 --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/DownstreamRoleDto.kt @@ -0,0 +1,29 @@ +/* + * 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.api2.model.dto + +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +internal data class DownstreamRoleDto( + val name: String, + val custom: Boolean = false, + val scopes: List = emptyList(), + val created_at: Date? = null, + val updated_at: Date? = null, +) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/SearchRolesResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/SearchRolesResponse.kt new file mode 100644 index 00000000000..f8be69e5043 --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/SearchRolesResponse.kt @@ -0,0 +1,26 @@ +/* + * 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.api2.model.response + +import com.squareup.moshi.JsonClass +import io.getstream.chat.android.client.api2.model.dto.DownstreamRoleDto + +@JsonClass(generateAdapter = true) +internal data class SearchRolesResponse( + val roles: List = emptyList(), + val duration: String? = null, +) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt index 53b6cf3e992..58809aeb045 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt @@ -51,6 +51,7 @@ import io.getstream.chat.android.client.api2.endpoint.OpenGraphApi import io.getstream.chat.android.client.api2.endpoint.PollsApi import io.getstream.chat.android.client.api2.endpoint.PushPreferencesApi import io.getstream.chat.android.client.api2.endpoint.RemindersApi +import io.getstream.chat.android.client.api2.endpoint.RoleApi import io.getstream.chat.android.client.api2.endpoint.ThreadsApi import io.getstream.chat.android.client.api2.endpoint.UserApi import io.getstream.chat.android.client.api2.endpoint.UserGroupApi @@ -354,6 +355,7 @@ constructor( buildRetrofitApi(), buildRetrofitApi(), buildRetrofitApi(), + buildRetrofitApi(), userScope, userScope, ).let { originalApi -> 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 bd366bed244..37935dc3061 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 @@ -48,6 +48,7 @@ import io.getstream.chat.android.client.api2.model.dto.DownstreamPushPreferenceD import io.getstream.chat.android.client.api2.model.dto.DownstreamReactionDto import io.getstream.chat.android.client.api2.model.dto.DownstreamReactionGroupDto import io.getstream.chat.android.client.api2.model.dto.DownstreamReminderDto +import io.getstream.chat.android.client.api2.model.dto.DownstreamRoleDto 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 @@ -1406,6 +1407,20 @@ internal object Mother { created_at = createdAt, updated_at = updatedAt, ) + + fun randomDownstreamRoleDto( + name: String = randomString(), + custom: Boolean = randomBoolean(), + scopes: List = emptyList(), + createdAt: Date? = randomDate(), + updatedAt: Date? = randomDate(), + ): DownstreamRoleDto = DownstreamRoleDto( + name = name, + custom = custom, + scopes = scopes, + created_at = createdAt, + updated_at = updatedAt, + ) } internal fun randomPushMessage( 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 98d4fc7bf80..b79477bdc14 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 @@ -31,6 +31,7 @@ import io.getstream.chat.android.client.api2.endpoint.OpenGraphApi import io.getstream.chat.android.client.api2.endpoint.PollsApi import io.getstream.chat.android.client.api2.endpoint.PushPreferencesApi import io.getstream.chat.android.client.api2.endpoint.RemindersApi +import io.getstream.chat.android.client.api2.endpoint.RoleApi import io.getstream.chat.android.client.api2.endpoint.ThreadsApi import io.getstream.chat.android.client.api2.endpoint.UserApi import io.getstream.chat.android.client.api2.endpoint.UserGroupApi @@ -116,6 +117,7 @@ import io.getstream.chat.android.client.api2.model.response.ReactionResponse import io.getstream.chat.android.client.api2.model.response.ReactionsResponse import io.getstream.chat.android.client.api2.model.response.ReminderResponse import io.getstream.chat.android.client.api2.model.response.SearchMessagesResponse +import io.getstream.chat.android.client.api2.model.response.SearchRolesResponse import io.getstream.chat.android.client.api2.model.response.SyncHistoryResponse import io.getstream.chat.android.client.api2.model.response.ThreadInfoResponse import io.getstream.chat.android.client.api2.model.response.ThreadResponse @@ -3131,6 +3133,41 @@ internal class MoshiChatApiTest { ) } + @ParameterizedTest + @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#searchRolesInput") + fun testSearchRoles(call: RetrofitCall, expected: KClass<*>) = runTest { + // given + val api = mock() + whenever( + api.searchRoles( + query = any(), + limit = anyOrNull(), + roleType = anyOrNull(), + includeGlobalRoles = anyOrNull(), + nameGt = anyOrNull(), + ), + ).doReturn(call) + val sut = Fixture() + .withRoleApi(api) + .get() + // when + val query = randomString() + val limit = randomInt() + val roleType = randomString() + val includeGlobalRoles = randomBoolean() + val nameGt = randomString() + val result = sut.searchRoles(query, limit, roleType, includeGlobalRoles, nameGt).await() + // then + result `should be instance of` expected + verify(api, times(1)).searchRoles( + query = query, + limit = limit, + roleType = roleType, + includeGlobalRoles = includeGlobalRoles, + nameGt = nameGt, + ) + } + private class Fixture { private var currentUserId: String = "" @@ -3153,6 +3190,7 @@ internal class MoshiChatApiTest { private var remindersApi: RemindersApi = mock() private var pushPreferencesApi: PushPreferencesApi = mock() private var userGroupApi: UserGroupApi = mock() + private var roleApi: RoleApi = mock() private var fileUploader: FileUploader = mock() private var fileTransformer: FileTransformer = NoOpFileTransformer @@ -3221,6 +3259,10 @@ internal class MoshiChatApiTest { this.userGroupApi = userGroupApi } + fun withRoleApi(roleApi: RoleApi) = apply { + this.roleApi = roleApi + } + fun withFileUploader(fileUploader: FileUploader) = apply { this.fileUploader = fileUploader } @@ -3257,6 +3299,7 @@ internal class MoshiChatApiTest { remindersApi = remindersApi, pushPreferencesApi = pushPreferencesApi, userGroupApi = userGroupApi, + roleApi = roleApi, userScope = UserScope(ClientScope()), coroutineScope = testCoroutineExtension.scope, ) 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 586d2a13aa2..c937239c463 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 @@ -59,6 +59,7 @@ import io.getstream.chat.android.client.api2.model.response.ReactionResponse import io.getstream.chat.android.client.api2.model.response.ReactionsResponse import io.getstream.chat.android.client.api2.model.response.ReminderResponse import io.getstream.chat.android.client.api2.model.response.SearchMessagesResponse +import io.getstream.chat.android.client.api2.model.response.SearchRolesResponse import io.getstream.chat.android.client.api2.model.response.UserGroupResponse import io.getstream.chat.android.client.api2.model.response.UserGroupsResponse import io.getstream.chat.android.client.api2.model.response.SyncHistoryResponse @@ -906,4 +907,15 @@ internal object MoshiChatApiTestArguments { @JvmStatic fun deleteUserGroupInput() = completableResponseArguments() + + @JvmStatic + fun searchRolesInput() = listOf( + Arguments.of( + RetroSuccess( + SearchRolesResponse(roles = listOf(Mother.randomDownstreamRoleDto())), + ).toRetrofitCall(), + Result.Success::class, + ), + Arguments.of(RetroError(statusCode = 500).toRetrofitCall(), Result.Failure::class), + ) } 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 2f46b0283f7..5402fe49cea 100644 --- a/stream-chat-android-core/api/stream-chat-android-core.api +++ b/stream-chat-android-core/api/stream-chat-android-core.api @@ -2088,6 +2088,26 @@ public final class io/getstream/chat/android/models/ReactionSortingBySumScore : public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/models/Role { + public fun (Ljava/lang/String;ZLjava/util/List;Ljava/util/Date;Ljava/util/Date;)V + public synthetic fun (Ljava/lang/String;ZLjava/util/List;Ljava/util/Date;Ljava/util/Date;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Z + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Ljava/util/Date; + public final fun component5 ()Ljava/util/Date; + public final fun copy (Ljava/lang/String;ZLjava/util/List;Ljava/util/Date;Ljava/util/Date;)Lio/getstream/chat/android/models/Role; + public static synthetic fun copy$default (Lio/getstream/chat/android/models/Role;Ljava/lang/String;ZLjava/util/List;Ljava/util/Date;Ljava/util/Date;ILjava/lang/Object;)Lio/getstream/chat/android/models/Role; + public fun equals (Ljava/lang/Object;)Z + public final fun getCreatedAt ()Ljava/util/Date; + public final fun getCustom ()Z + public final fun getName ()Ljava/lang/String; + public final fun getScopes ()Ljava/util/List; + public final fun getUpdatedAt ()Ljava/util/Date; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/models/SearchMessagesResult { public fun ()V public fun (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/SearchWarning;)V diff --git a/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Role.kt b/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Role.kt new file mode 100644 index 00000000000..9a768700275 --- /dev/null +++ b/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Role.kt @@ -0,0 +1,38 @@ +/* + * 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.models + +import androidx.compose.runtime.Immutable +import java.util.Date + +/** + * App-level role definition. + * + * @param name Role identifier. + * @param custom `true` for app-defined roles, `false` for built-ins (`admin`, `user`, etc.). + * @param scopes Scopes this role has grants in. + * @param createdAt When the role was created. + * @param updatedAt Last time the role was updated. + */ +@Immutable +public data class Role( + val name: String, + val custom: Boolean = false, + val scopes: List = emptyList(), + val createdAt: Date? = null, + val updatedAt: Date? = null, +) From 6b6922742dfabfd47db182dcc224a50cf7bc9be1 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:39:25 +0200 Subject: [PATCH 3/5] Expose granular ChatPreferences for push notifications Adds ChatPreferences with per-category toggles (direct, role, group, here, channel mentions, thread replies, default fallback) alongside the existing coarse PushPreferenceLevel. New ChatClient methods setUserChatPreferences and setChannelChatPreferences hit the existing /push_preferences endpoint with the chat_preferences payload, and the response now hydrates PushPreference.chatPreferences so callers can read back the active toggles. --- .../api/stream-chat-android-client.api | 2 + .../chat/android/client/ChatClient.kt | 27 ++++++ .../chat/android/client/api/ChatApi.kt | 7 ++ .../chat/android/client/api2/MoshiChatApi.kt | 27 ++++++ .../client/api2/mapping/DomainMapping.kt | 14 +++ .../android/client/api2/mapping/DtoMapping.kt | 12 +++ .../api2/model/dto/PushPreferenceDtos.kt | 26 ++++++ .../android/client/api2/MoshiChatApiTest.kt | 91 ++++++++++++++++++- .../api/stream-chat-android-core.api | 52 ++++++++++- .../chat/android/models/ChatPreferences.kt | 54 +++++++++++ .../chat/android/models/PushPreference.kt | 7 +- 11 files changed, 311 insertions(+), 8 deletions(-) create mode 100644 stream-chat-android-core/src/main/java/io/getstream/chat/android/models/ChatPreferences.kt 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 d26c15f9110..202f4d97025 100644 --- a/stream-chat-android-client/api/stream-chat-android-client.api +++ b/stream-chat-android-client/api/stream-chat-android-client.api @@ -214,9 +214,11 @@ public final class io/getstream/chat/android/client/ChatClient { public final fun sendReaction (Lio/getstream/chat/android/models/Reaction;ZLjava/lang/String;Z)Lio/getstream/result/call/Call; public static synthetic fun sendReaction$default (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/Reaction;ZLjava/lang/String;ZILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun sendStaticLocation (Ljava/lang/String;DDLjava/lang/String;)Lio/getstream/result/call/Call; + public final fun setChannelChatPreferences (Ljava/lang/String;Lio/getstream/chat/android/models/ChatPreferences;)Lio/getstream/result/call/Call; public final fun setChannelPushPreference (Ljava/lang/String;Lio/getstream/chat/android/models/PushPreferenceLevel;)Lio/getstream/result/call/Call; public final fun setLogicRegistry (Lio/getstream/chat/android/client/channel/state/ChannelStateLogicProvider;)V public static final fun setOFFLINE_SUPPORT_ENABLED (Z)V + public final fun setUserChatPreferences (Lio/getstream/chat/android/models/ChatPreferences;)Lio/getstream/result/call/Call; public final fun setUserPushPreference (Lio/getstream/chat/android/models/PushPreferenceLevel;)Lio/getstream/result/call/Call; public static final fun setVERSION_PREFIX_HEADER (Lio/getstream/chat/android/client/header/VersionPrefixHeader;)V public final fun shadowBanUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)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 e86a2e498ea..5e822463d35 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 @@ -181,6 +181,7 @@ import io.getstream.chat.android.models.Attachment import io.getstream.chat.android.models.BannedUser import io.getstream.chat.android.models.BannedUsersSort import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.models.ChatPreferences import io.getstream.chat.android.models.ConnectionData import io.getstream.chat.android.models.ConnectionState import io.getstream.chat.android.models.CreatePollParams @@ -1732,6 +1733,32 @@ internal constructor( } } + /** + * Sets per-category push toggles for the current user. Use this when + * [setUserPushPreference] is too coarse. Setting either clears the other server-side. + */ + @CheckResult + public fun setUserChatPreferences(preferences: ChatPreferences): Call { + return api.setUserChatPreferences(preferences) + .doOnResult(userScope) { result -> + if (result is Result.Success) { + val currentUser = mutableClientState.user.value ?: return@doOnResult + val updatedUser = currentUser.copy(pushPreference = result.value) + mutableClientState.setUser(updatedUser) + } + } + } + + /** + * Per-channel version of [setUserChatPreferences]. + * + * @param cid Full channel identifier (e.g. `messaging:123`). + */ + @CheckResult + public fun setChannelChatPreferences(cid: String, preferences: ChatPreferences): Call { + return api.setChannelChatPreferences(cid, preferences) + } + /** * Creates a user group. * 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 a648fd368b4..43552546b01 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 @@ -33,6 +33,7 @@ import io.getstream.chat.android.models.Attachment import io.getstream.chat.android.models.BannedUser import io.getstream.chat.android.models.BannedUsersSort import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.models.ChatPreferences import io.getstream.chat.android.models.CreatePollParams import io.getstream.chat.android.models.Device import io.getstream.chat.android.models.DraftMessage @@ -146,6 +147,12 @@ internal interface ChatApi { @CheckResult fun snoozeChannelPushNotifications(cid: String, until: Date): Call + @CheckResult + fun setUserChatPreferences(preferences: ChatPreferences): Call + + @CheckResult + fun setChannelChatPreferences(cid: String, preferences: ChatPreferences): Call + @CheckResult fun createUserGroup( name: String, 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 6e631ac3b5b..6daba741e1d 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 @@ -125,6 +125,7 @@ import io.getstream.chat.android.models.Attachment import io.getstream.chat.android.models.BannedUser import io.getstream.chat.android.models.BannedUsersSort import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.models.ChatPreferences import io.getstream.chat.android.models.CreatePollParams import io.getstream.chat.android.models.Device import io.getstream.chat.android.models.DraftMessage @@ -531,6 +532,32 @@ constructor( .parseChannelPushPreferencesResponse(cid) } + override fun setUserChatPreferences(preferences: ChatPreferences): Call { + val input = UpstreamPushPreferenceInputDto( + channel_cid = null, + chat_level = null, + disabled_until = null, + remove_disable = null, + chat_preferences = with(dtoMapping) { preferences.toDto() }, + ) + return pushPreferencesApi + .upsertPushPreferences(UpsertPushPreferencesRequest(listOf(input))) + .parseUserPushPreferencesResponse() + } + + override fun setChannelChatPreferences(cid: String, preferences: ChatPreferences): Call { + val input = UpstreamPushPreferenceInputDto( + channel_cid = cid, + chat_level = null, + disabled_until = null, + remove_disable = null, + chat_preferences = with(dtoMapping) { preferences.toDto() }, + ) + return pushPreferencesApi + .upsertPushPreferences(UpsertPushPreferencesRequest(listOf(input))) + .parseChannelPushPreferencesResponse(cid) + } + override fun createUserGroup( name: String, id: String?, 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 e5c162b7276..f601748b6ff 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 @@ -28,6 +28,7 @@ import io.getstream.chat.android.client.api2.model.dto.DeliveryReceiptsDto import io.getstream.chat.android.client.api2.model.dto.DeviceDto import io.getstream.chat.android.client.api2.model.dto.DownstreamChannelDto import io.getstream.chat.android.client.api2.model.dto.DownstreamChannelMuteDto +import io.getstream.chat.android.client.api2.model.dto.DownstreamChatPreferencesDto import io.getstream.chat.android.client.api2.model.dto.DownstreamChannelUserRead import io.getstream.chat.android.client.api2.model.dto.DownstreamDraftDto import io.getstream.chat.android.client.api2.model.dto.DownstreamFlagDto @@ -85,6 +86,8 @@ import io.getstream.chat.android.models.ChannelInfo import io.getstream.chat.android.models.ChannelMute import io.getstream.chat.android.models.ChannelTransformer import io.getstream.chat.android.models.ChannelUserRead +import io.getstream.chat.android.models.ChatPreferenceToggle +import io.getstream.chat.android.models.ChatPreferences import io.getstream.chat.android.models.Command import io.getstream.chat.android.models.Config import io.getstream.chat.android.models.Device @@ -916,6 +919,17 @@ internal class DomainMapping( internal fun DownstreamPushPreferenceDto.toDomain(): PushPreference = PushPreference( level = PushPreferenceLevel.fromValue(chat_level), disabledUntil = disabled_until, + chatPreferences = chat_preferences?.toDomain(), + ) + + internal fun DownstreamChatPreferencesDto.toDomain(): ChatPreferences = ChatPreferences( + directMentions = ChatPreferenceToggle.fromValue(direct_mentions), + roleMentions = ChatPreferenceToggle.fromValue(role_mentions), + groupMentions = ChatPreferenceToggle.fromValue(group_mentions), + hereMentions = ChatPreferenceToggle.fromValue(here_mentions), + channelMentions = ChatPreferenceToggle.fromValue(channel_mentions), + threadReplies = ChatPreferenceToggle.fromValue(thread_replies), + defaultPreference = ChatPreferenceToggle.fromValue(default_preference), ) internal fun List>?.toSortDomain(): QuerySorter? { diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DtoMapping.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DtoMapping.kt index 1f86c282f4d..ef2bd24df3b 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DtoMapping.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DtoMapping.kt @@ -26,6 +26,7 @@ import io.getstream.chat.android.client.api2.model.dto.DeviceDto import io.getstream.chat.android.client.api2.model.dto.PrivacySettingsDto import io.getstream.chat.android.client.api2.model.dto.ReadReceiptsDto import io.getstream.chat.android.client.api2.model.dto.TypingIndicatorsDto +import io.getstream.chat.android.client.api2.model.dto.UpstreamChatPreferencesDto import io.getstream.chat.android.client.api2.model.dto.UpstreamConnectedEventDto import io.getstream.chat.android.client.api2.model.dto.UpstreamLocationDto import io.getstream.chat.android.client.api2.model.dto.UpstreamMemberDataDto @@ -36,6 +37,7 @@ import io.getstream.chat.android.client.api2.model.dto.UpstreamReactionDto import io.getstream.chat.android.client.api2.model.dto.UpstreamUserDto import io.getstream.chat.android.client.events.ConnectedEvent import io.getstream.chat.android.models.Attachment +import io.getstream.chat.android.models.ChatPreferences import io.getstream.chat.android.models.Device import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.Location @@ -277,4 +279,14 @@ internal class DtoMapping( me = me.toDto(), connection_id = connectionId, ) + + internal fun ChatPreferences.toDto(): UpstreamChatPreferencesDto = UpstreamChatPreferencesDto( + direct_mentions = directMentions?.value, + role_mentions = roleMentions?.value, + group_mentions = groupMentions?.value, + here_mentions = hereMentions?.value, + channel_mentions = channelMentions?.value, + thread_replies = threadReplies?.value, + default_preference = defaultPreference?.value, + ) } diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/PushPreferenceDtos.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/PushPreferenceDtos.kt index b304a05c70a..94d855855a9 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/PushPreferenceDtos.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/PushPreferenceDtos.kt @@ -26,6 +26,7 @@ import java.util.Date * @param chat_level The chat level preference ("all", "default", "mentions" or "none"). * @param disabled_until Timestamp until which notifications are disabled. * @param remove_disable Whether to remove any existing disable setting. + * @param chat_preferences Per-category toggles. Setting this clears [chat_level] server-side. */ @JsonClass(generateAdapter = true) internal data class UpstreamPushPreferenceInputDto( @@ -33,6 +34,7 @@ internal data class UpstreamPushPreferenceInputDto( val chat_level: String?, val disabled_until: Date?, val remove_disable: Boolean?, + val chat_preferences: UpstreamChatPreferencesDto? = null, ) /** @@ -40,9 +42,33 @@ internal data class UpstreamPushPreferenceInputDto( * * @param chat_level The chat level preference ("all", "default", "mentions" or "none"). * @param disabled_until Timestamp until which notifications are disabled. + * @param chat_preferences Per-category toggles when set instead of [chat_level]. */ @JsonClass(generateAdapter = true) internal data class DownstreamPushPreferenceDto( val chat_level: String?, val disabled_until: Date?, + val chat_preferences: DownstreamChatPreferencesDto? = null, +) + +@JsonClass(generateAdapter = true) +internal data class UpstreamChatPreferencesDto( + val direct_mentions: String? = null, + val role_mentions: String? = null, + val group_mentions: String? = null, + val here_mentions: String? = null, + val channel_mentions: String? = null, + val thread_replies: String? = null, + val default_preference: String? = null, +) + +@JsonClass(generateAdapter = true) +internal data class DownstreamChatPreferencesDto( + val direct_mentions: String? = null, + val role_mentions: String? = null, + val group_mentions: String? = null, + val here_mentions: String? = null, + val channel_mentions: String? = null, + val thread_replies: String? = null, + val default_preference: String? = null, ) 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 b79477bdc14..048fe1dd611 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 @@ -39,10 +39,12 @@ import io.getstream.chat.android.client.api2.mapping.DomainMapping import io.getstream.chat.android.client.api2.mapping.DtoMapping import io.getstream.chat.android.client.api2.mapping.EventMapping import io.getstream.chat.android.client.api2.model.dto.AttachmentDto +import io.getstream.chat.android.client.api2.model.dto.DownstreamChatPreferencesDto import io.getstream.chat.android.client.api2.model.dto.DownstreamLocationDto import io.getstream.chat.android.client.api2.model.dto.DownstreamPushPreferenceDto import io.getstream.chat.android.client.api2.model.dto.PartialUpdateUserDto import io.getstream.chat.android.client.api2.model.dto.UnreadDto +import io.getstream.chat.android.client.api2.model.dto.UpstreamChatPreferencesDto import io.getstream.chat.android.client.api2.model.dto.UpstreamPushPreferenceInputDto import io.getstream.chat.android.client.api2.model.requests.AcceptInviteRequest import io.getstream.chat.android.client.api2.model.requests.AddDeviceRequest @@ -150,6 +152,8 @@ 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.models.Poll +import io.getstream.chat.android.models.ChatPreferenceToggle +import io.getstream.chat.android.models.ChatPreferences import io.getstream.chat.android.models.PushPreferenceLevel import io.getstream.chat.android.models.Reaction import io.getstream.chat.android.models.UnreadCounts @@ -2862,6 +2866,91 @@ internal class MoshiChatApiTest { verify(api, times(1)).upsertPushPreferences(expectedRequest) } + @Test + fun testSetUserChatPreferences() = runTest { + val userId = randomString() + val prefs = ChatPreferences( + directMentions = ChatPreferenceToggle.all, + channelMentions = ChatPreferenceToggle.none, + defaultPreference = ChatPreferenceToggle.none, + ) + val response = PushPreferencesResponse( + user_channel_preferences = emptyMap(), + user_preferences = mapOf( + userId to DownstreamPushPreferenceDto( + chat_level = null, + disabled_until = null, + chat_preferences = DownstreamChatPreferencesDto( + direct_mentions = "all", + channel_mentions = "none", + default_preference = "none", + ), + ), + ), + ) + val api = mock() + whenever(api.upsertPushPreferences(any())).doReturn(RetroSuccess(response).toRetrofitCall()) + val sut = Fixture().withCurrentUserId(userId).withPushPreferencesApi(api).get() + + val result = sut.setUserChatPreferences(prefs).await() + + val expectedRequest = UpsertPushPreferencesRequest( + preferences = listOf( + UpstreamPushPreferenceInputDto( + channel_cid = null, + chat_level = null, + disabled_until = null, + remove_disable = null, + chat_preferences = UpstreamChatPreferencesDto( + direct_mentions = "all", + channel_mentions = "none", + default_preference = "none", + ), + ), + ), + ) + Assertions.assertInstanceOf(Result.Success::class.java, result) + verify(api, times(1)).upsertPushPreferences(expectedRequest) + } + + @Test + fun testSetChannelChatPreferences() = runTest { + val userId = randomString() + val cid = randomCID() + val prefs = ChatPreferences(directMentions = ChatPreferenceToggle.all) + val response = PushPreferencesResponse( + user_channel_preferences = mapOf( + userId to mapOf( + cid to DownstreamPushPreferenceDto( + chat_level = null, + disabled_until = null, + chat_preferences = DownstreamChatPreferencesDto(direct_mentions = "all"), + ), + ), + ), + user_preferences = emptyMap(), + ) + val api = mock() + whenever(api.upsertPushPreferences(any())).doReturn(RetroSuccess(response).toRetrofitCall()) + val sut = Fixture().withCurrentUserId(userId).withPushPreferencesApi(api).get() + + val result = sut.setChannelChatPreferences(cid, prefs).await() + + val expectedRequest = UpsertPushPreferencesRequest( + preferences = listOf( + UpstreamPushPreferenceInputDto( + channel_cid = cid, + chat_level = null, + disabled_until = null, + remove_disable = null, + chat_preferences = UpstreamChatPreferencesDto(direct_mentions = "all"), + ), + ), + ) + Assertions.assertInstanceOf(Result.Success::class.java, result) + verify(api, times(1)).upsertPushPreferences(expectedRequest) + } + @ParameterizedTest @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#updateLiveLocation") fun testUpdateLiveLocation( @@ -2967,7 +3056,7 @@ internal class MoshiChatApiTest { @ParameterizedTest @MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#userGroupsResponseInput") - fun testQueryUserGroups(call: RetrofitCall, expected: KClass<*>) = runTest { + fun testListUserGroups(call: RetrofitCall, expected: KClass<*>) = runTest { val api = mock() whenever( api.queryUserGroups( 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 5402fe49cea..3052022fbb1 100644 --- a/stream-chat-android-core/api/stream-chat-android-core.api +++ b/stream-chat-android-core/api/stream-chat-android-core.api @@ -656,6 +656,49 @@ public final class io/getstream/chat/android/models/ChannelUserRead : io/getstre public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/models/ChatPreferenceToggle { + public static final field Companion Lio/getstream/chat/android/models/ChatPreferenceToggle$Companion; + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lio/getstream/chat/android/models/ChatPreferenceToggle; + public static synthetic fun copy$default (Lio/getstream/chat/android/models/ChatPreferenceToggle;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/chat/android/models/ChatPreferenceToggle; + public fun equals (Ljava/lang/Object;)Z + public final fun getValue ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/chat/android/models/ChatPreferenceToggle$Companion { + public final fun fromValue (Ljava/lang/String;)Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun getAll ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun getNone ()Lio/getstream/chat/android/models/ChatPreferenceToggle; +} + +public final class io/getstream/chat/android/models/ChatPreferences { + public fun ()V + public fun (Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;)V + public synthetic fun (Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun component2 ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun component3 ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun component4 ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun component5 ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun component6 ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun component7 ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun copy (Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;)Lio/getstream/chat/android/models/ChatPreferences; + public static synthetic fun copy$default (Lio/getstream/chat/android/models/ChatPreferences;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;Lio/getstream/chat/android/models/ChatPreferenceToggle;ILjava/lang/Object;)Lio/getstream/chat/android/models/ChatPreferences; + public fun equals (Ljava/lang/Object;)Z + public final fun getChannelMentions ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun getDefaultPreference ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun getDirectMentions ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun getGroupMentions ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun getHereMentions ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun getRoleMentions ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public final fun getThreadReplies ()Lio/getstream/chat/android/models/ChatPreferenceToggle; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/models/Command { public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V public final fun component1 ()Ljava/lang/String; @@ -1827,12 +1870,15 @@ public final class io/getstream/chat/android/models/PushMessage { } public final class io/getstream/chat/android/models/PushPreference { - public fun (Lio/getstream/chat/android/models/PushPreferenceLevel;Ljava/util/Date;)V + public fun (Lio/getstream/chat/android/models/PushPreferenceLevel;Ljava/util/Date;Lio/getstream/chat/android/models/ChatPreferences;)V + public synthetic fun (Lio/getstream/chat/android/models/PushPreferenceLevel;Ljava/util/Date;Lio/getstream/chat/android/models/ChatPreferences;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/models/PushPreferenceLevel; public final fun component2 ()Ljava/util/Date; - public final fun copy (Lio/getstream/chat/android/models/PushPreferenceLevel;Ljava/util/Date;)Lio/getstream/chat/android/models/PushPreference; - public static synthetic fun copy$default (Lio/getstream/chat/android/models/PushPreference;Lio/getstream/chat/android/models/PushPreferenceLevel;Ljava/util/Date;ILjava/lang/Object;)Lio/getstream/chat/android/models/PushPreference; + public final fun component3 ()Lio/getstream/chat/android/models/ChatPreferences; + public final fun copy (Lio/getstream/chat/android/models/PushPreferenceLevel;Ljava/util/Date;Lio/getstream/chat/android/models/ChatPreferences;)Lio/getstream/chat/android/models/PushPreference; + public static synthetic fun copy$default (Lio/getstream/chat/android/models/PushPreference;Lio/getstream/chat/android/models/PushPreferenceLevel;Ljava/util/Date;Lio/getstream/chat/android/models/ChatPreferences;ILjava/lang/Object;)Lio/getstream/chat/android/models/PushPreference; public fun equals (Ljava/lang/Object;)Z + public final fun getChatPreferences ()Lio/getstream/chat/android/models/ChatPreferences; public final fun getDisabledUntil ()Ljava/util/Date; public final fun getLevel ()Lio/getstream/chat/android/models/PushPreferenceLevel; public fun hashCode ()I diff --git a/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/ChatPreferences.kt b/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/ChatPreferences.kt new file mode 100644 index 00000000000..537cdefdb53 --- /dev/null +++ b/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/ChatPreferences.kt @@ -0,0 +1,54 @@ +/* + * 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.models + +/** + * Per-category push toggles. A null field means "use the server default". When set on a + * [PushPreference], takes precedence over [PushPreference.level]. + */ +public data class ChatPreferences( + public val directMentions: ChatPreferenceToggle? = null, + public val roleMentions: ChatPreferenceToggle? = null, + public val groupMentions: ChatPreferenceToggle? = null, + public val hereMentions: ChatPreferenceToggle? = null, + public val channelMentions: ChatPreferenceToggle? = null, + public val threadReplies: ChatPreferenceToggle? = null, + public val defaultPreference: ChatPreferenceToggle? = null, +) + +/** + * Per-category toggle for [ChatPreferences]. Wraps the raw wire string so unknown values + * round-trip without being lost. + */ +public data class ChatPreferenceToggle(public val value: String) { + + public companion object { + + /** Receive push for this category. */ + public val all: ChatPreferenceToggle = ChatPreferenceToggle(value = "all") + + /** Suppress push for this category. */ + public val none: ChatPreferenceToggle = ChatPreferenceToggle(value = "none") + + public fun fromValue(value: String?): ChatPreferenceToggle? = when (value) { + null, "" -> null + all.value -> all + none.value -> none + else -> ChatPreferenceToggle(value) + } + } +} diff --git a/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/PushPreference.kt b/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/PushPreference.kt index 5b2949d5f15..5aa9d97e47e 100644 --- a/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/PushPreference.kt +++ b/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/PushPreference.kt @@ -21,15 +21,14 @@ import java.util.Date /** * Represents the push notification preference for a specific user or channel. * - * @param level The chat level preference for notifications. Possible values are: - * - [PushPreferenceLevel.all]: Receive notifications for all messages. - * - [PushPreferenceLevel.mentions]: Receive notifications only for mentions. - * - [PushPreferenceLevel.none]: Do not receive any notifications. + * @param level Coarse chat level. Ignored when [chatPreferences] is set. * @param disabledUntil Timestamp until which notifications are disabled. If null, notifications are not disabled. + * @param chatPreferences Per-category toggles. Takes precedence over [level] when set. */ public data class PushPreference( public val level: PushPreferenceLevel?, public val disabledUntil: Date?, + public val chatPreferences: ChatPreferences? = null, ) /** From 651a5a6bd4b75507e68afa686ee08f9c703833db Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:54:10 +0200 Subject: [PATCH 4/5] Static checks fix --- .../chat/android/client/api2/mapping/DomainMapping.kt | 2 +- .../io/getstream/chat/android/client/api2/MoshiChatApiTest.kt | 4 ++-- .../chat/android/client/api2/MoshiChatApiTestArguments.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) 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 f601748b6ff..4b0e3e47678 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 @@ -28,8 +28,8 @@ import io.getstream.chat.android.client.api2.model.dto.DeliveryReceiptsDto import io.getstream.chat.android.client.api2.model.dto.DeviceDto import io.getstream.chat.android.client.api2.model.dto.DownstreamChannelDto import io.getstream.chat.android.client.api2.model.dto.DownstreamChannelMuteDto -import io.getstream.chat.android.client.api2.model.dto.DownstreamChatPreferencesDto import io.getstream.chat.android.client.api2.model.dto.DownstreamChannelUserRead +import io.getstream.chat.android.client.api2.model.dto.DownstreamChatPreferencesDto import io.getstream.chat.android.client.api2.model.dto.DownstreamDraftDto import io.getstream.chat.android.client.api2.model.dto.DownstreamFlagDto import io.getstream.chat.android.client.api2.model.dto.DownstreamLocationDto 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 048fe1dd611..bafc6452d3d 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 @@ -143,6 +143,8 @@ import io.getstream.chat.android.client.utils.verifyNetworkError import io.getstream.chat.android.client.utils.verifySuccess import io.getstream.chat.android.models.BannedUsersSort import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.models.ChatPreferenceToggle +import io.getstream.chat.android.models.ChatPreferences import io.getstream.chat.android.models.Filters import io.getstream.chat.android.models.Location import io.getstream.chat.android.models.Member @@ -152,8 +154,6 @@ 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.models.Poll -import io.getstream.chat.android.models.ChatPreferenceToggle -import io.getstream.chat.android.models.ChatPreferences import io.getstream.chat.android.models.PushPreferenceLevel import io.getstream.chat.android.models.Reaction import io.getstream.chat.android.models.UnreadCounts 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 c937239c463..5f3d55c1490 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 @@ -60,13 +60,13 @@ import io.getstream.chat.android.client.api2.model.response.ReactionsResponse import io.getstream.chat.android.client.api2.model.response.ReminderResponse import io.getstream.chat.android.client.api2.model.response.SearchMessagesResponse import io.getstream.chat.android.client.api2.model.response.SearchRolesResponse -import io.getstream.chat.android.client.api2.model.response.UserGroupResponse -import io.getstream.chat.android.client.api2.model.response.UserGroupsResponse import io.getstream.chat.android.client.api2.model.response.SyncHistoryResponse import io.getstream.chat.android.client.api2.model.response.ThreadInfoResponse import io.getstream.chat.android.client.api2.model.response.ThreadResponse import io.getstream.chat.android.client.api2.model.response.TokenResponse import io.getstream.chat.android.client.api2.model.response.UpdateUsersResponse +import io.getstream.chat.android.client.api2.model.response.UserGroupResponse +import io.getstream.chat.android.client.api2.model.response.UserGroupsResponse import io.getstream.chat.android.client.api2.model.response.UsersResponse import io.getstream.chat.android.client.utils.RetroError import io.getstream.chat.android.client.utils.RetroSuccess From 306aa68501d798643836526cf0434c54decaf011 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:47:16 +0200 Subject: [PATCH 5/5] Propagate setChannelChatPreferences to plugin/channel state --- .../chat/android/client/ChatClient.kt | 3 ++ .../internal/PushPreferencesListenerState.kt | 11 +++++ .../chat/android/client/plugin/Plugin.kt | 9 ++++ .../listeners/PushPreferencesListener.kt | 15 +++++++ .../ChatClientPushPreferencesApiTest.kt | 42 +++++++++++++++++++ .../PushPreferencesListenerStateTest.kt | 38 +++++++++++++++-- 6 files changed, 114 insertions(+), 4 deletions(-) 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 5e822463d35..7fd732aea75 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 @@ -1757,6 +1757,9 @@ internal constructor( @CheckResult public fun setChannelChatPreferences(cid: String, preferences: ChatPreferences): Call { return api.setChannelChatPreferences(cid, preferences) + .doOnResult(userScope) { result -> + plugins.forEach { it.onChannelChatPreferencesSet(cid, preferences, result) } + } } /** diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/PushPreferencesListenerState.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/PushPreferencesListenerState.kt index 3e4f4c57b45..df30b0d1ed3 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/PushPreferencesListenerState.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/PushPreferencesListenerState.kt @@ -19,6 +19,7 @@ package io.getstream.chat.android.client.internal.state.plugin.listener.internal import io.getstream.chat.android.client.extensions.cidToTypeAndId import io.getstream.chat.android.client.internal.state.plugin.logic.internal.LogicRegistry import io.getstream.chat.android.client.plugin.listeners.PushPreferencesListener +import io.getstream.chat.android.models.ChatPreferences import io.getstream.chat.android.models.PushPreference import io.getstream.chat.android.models.PushPreferenceLevel import io.getstream.result.Result @@ -47,6 +48,16 @@ internal class PushPreferencesListenerState(private val logic: LogicRegistry) : } } + override suspend fun onChannelChatPreferencesSet( + cid: String, + preferences: ChatPreferences, + result: Result, + ) { + result.onSuccess { pushPreference -> + updateChannelPushPreference(cid, pushPreference) + } + } + private fun updateChannelPushPreference(cid: String, pushPreference: PushPreference) { val (type, id) = cid.cidToTypeAndId() logic.channel(type, id).setPushPreference(pushPreference) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/Plugin.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/Plugin.kt index cfa35def43f..cc0b14e64a6 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/Plugin.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/Plugin.kt @@ -52,6 +52,7 @@ import io.getstream.chat.android.client.plugin.listeners.UnblockUserListener import io.getstream.chat.android.client.query.CreateChannelParams import io.getstream.chat.android.core.internal.InternalStreamChatApi import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.models.ChatPreferences import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.DraftsSort import io.getstream.chat.android.models.FilterObject @@ -493,4 +494,12 @@ public interface Plugin : ) { /* No-Op */ } + + override suspend fun onChannelChatPreferencesSet( + cid: String, + preferences: ChatPreferences, + result: Result, + ) { + /* No-Op */ + } } diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/listeners/PushPreferencesListener.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/listeners/PushPreferencesListener.kt index 724f7c29c8c..164cd8d352e 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/listeners/PushPreferencesListener.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/listeners/PushPreferencesListener.kt @@ -17,6 +17,7 @@ package io.getstream.chat.android.client.plugin.listeners import io.getstream.chat.android.core.internal.InternalStreamChatApi +import io.getstream.chat.android.models.ChatPreferences import io.getstream.chat.android.models.PushPreference import io.getstream.chat.android.models.PushPreferenceLevel import io.getstream.result.Result @@ -55,4 +56,18 @@ public interface PushPreferencesListener { until: Date, result: Result, ) + + /** + * Called when per-category chat preferences are set for a given channel. + * + * @param cid The full channel ID (e.g., "messaging:123"). + * @param preferences The [ChatPreferences] applied to the channel. + * @param result The result of the update operation, containing the updated [PushPreference] on success or an error + * on failure. + */ + public suspend fun onChannelChatPreferencesSet( + cid: String, + preferences: ChatPreferences, + result: Result, + ) } diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientPushPreferencesApiTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientPushPreferencesApiTest.kt index d898f60b349..ed0368c23ad 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientPushPreferencesApiTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientPushPreferencesApiTest.kt @@ -22,6 +22,8 @@ import io.getstream.chat.android.client.utils.RetroError import io.getstream.chat.android.client.utils.RetroSuccess import io.getstream.chat.android.client.utils.verifyNetworkError import io.getstream.chat.android.client.utils.verifySuccess +import io.getstream.chat.android.models.ChatPreferenceToggle +import io.getstream.chat.android.models.ChatPreferences import io.getstream.chat.android.models.PushPreference import io.getstream.chat.android.models.PushPreferenceLevel import io.getstream.chat.android.models.User @@ -196,4 +198,44 @@ internal class ChatClientPushPreferencesApiTest : BaseChatClientTest() { verifyNetworkError(result, errorCode) verify(mockPlugin).onChannelPushNotificationsSnoozed(cid, until, result) } + + @Test + fun `setChannelChatPreferences success should return push preference and notify plugins`() = runTest { + // given + val cid = "messaging:${randomString()}" + val preferences = ChatPreferences(directMentions = ChatPreferenceToggle.all) + val pushPreference = PushPreference(level = null, disabledUntil = null, chatPreferences = preferences) + val mockPlugin = mock() + plugins.add(mockPlugin) + + whenever(api.setChannelChatPreferences(cid, preferences)) + .doReturn(RetroSuccess(pushPreference).toRetrofitCall()) + + // when + val result = chatClient.setChannelChatPreferences(cid, preferences).await() + + // then + verifySuccess(result, pushPreference) + verify(mockPlugin).onChannelChatPreferencesSet(cid, preferences, Result.Success(pushPreference)) + } + + @Test + fun `setChannelChatPreferences error should return error and notify plugins`() = runTest { + // given + val cid = "messaging:${randomString()}" + val preferences = ChatPreferences(directMentions = ChatPreferenceToggle.all) + val errorCode = positiveRandomInt() + val mockPlugin = mock() + plugins.add(mockPlugin) + + whenever(api.setChannelChatPreferences(cid, preferences)) + .doReturn(RetroError(errorCode).toRetrofitCall()) + + // when + val result = chatClient.setChannelChatPreferences(cid, preferences).await() + + // then + verifyNetworkError(result, errorCode) + verify(mockPlugin).onChannelChatPreferencesSet(cid, preferences, result) + } } diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/PushPreferencesListenerStateTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/PushPreferencesListenerStateTest.kt index a28e6b58a90..39bc7121e69 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/PushPreferencesListenerStateTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/PushPreferencesListenerStateTest.kt @@ -18,6 +18,8 @@ package io.getstream.chat.android.client.internal.state.plugin.listener.internal import io.getstream.chat.android.client.internal.state.plugin.logic.channel.internal.ChannelLogic import io.getstream.chat.android.client.internal.state.plugin.logic.internal.LogicRegistry +import io.getstream.chat.android.models.ChatPreferenceToggle +import io.getstream.chat.android.models.ChatPreferences import io.getstream.chat.android.models.PushPreference import io.getstream.chat.android.models.PushPreferenceLevel import io.getstream.result.Error @@ -42,7 +44,7 @@ internal class PushPreferencesListenerStateTest { private val sut: PushPreferencesListenerState = PushPreferencesListenerState(logicRegistry) @Test - fun `onChannelPushPreferenceSet with success calls updateChannelData`() = runTest { + fun `onChannelPushPreferenceSet with success calls setPushPreference`() = runTest { // Given val cid = "messaging:channel123" val level = PushPreferenceLevel.all @@ -57,7 +59,7 @@ internal class PushPreferencesListenerStateTest { } @Test - fun `onChannelPushPreferenceSet with failure does not call updateChannelData`() = runTest { + fun `onChannelPushPreferenceSet with failure does not call setPushPreference`() = runTest { // Given val cid = "messaging:channel123" val level = PushPreferenceLevel.all @@ -70,7 +72,7 @@ internal class PushPreferencesListenerStateTest { } @Test - fun `onChannelPushNotificationsSnoozed with success calls updateChannelData`() = runTest { + fun `onChannelPushNotificationsSnoozed with success calls setPushPreference`() = runTest { // Given val cid = "messaging:channel456" val until = Date() @@ -85,7 +87,7 @@ internal class PushPreferencesListenerStateTest { } @Test - fun `onChannelPushNotificationsSnoozed with failure does not call updateChannelData`() = runTest { + fun `onChannelPushNotificationsSnoozed with failure does not call setPushPreference`() = runTest { // Given val cid = "messaging:channel456" val until = Date() @@ -96,4 +98,32 @@ internal class PushPreferencesListenerStateTest { // Then verify(channelLogic, times(0)).setPushPreference(any()) } + + @Test + fun `onChannelChatPreferencesSet with success calls setPushPreference`() = runTest { + // Given + val cid = "messaging:channel789" + val preferences = ChatPreferences(directMentions = ChatPreferenceToggle.all) + val preference = PushPreference(level = null, disabledUntil = null, chatPreferences = preferences) + doNothing().whenever(channelLogic).setPushPreference(preference) + + // When + sut.onChannelChatPreferencesSet(cid, preferences, Result.Success(preference)) + + // Then + verify(channelLogic, times(1)).setPushPreference(preference) + } + + @Test + fun `onChannelChatPreferencesSet with failure does not call setPushPreference`() = runTest { + // Given + val cid = "messaging:channel789" + val preferences = ChatPreferences(directMentions = ChatPreferenceToggle.all) + + // When + sut.onChannelChatPreferencesSet(cid, preferences, Result.Failure(Error.GenericError("error"))) + + // Then + verify(channelLogic, times(0)).setPushPreference(any()) + } }