diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/DirectEventParser.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/DirectEventParser.kt index 7da3cb8b43f..a19014638d8 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/DirectEventParser.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/DirectEventParser.kt @@ -36,6 +36,8 @@ import io.getstream.chat.android.client.parser2.direct.PrivacySettingsAdapter import io.getstream.chat.android.client.parser2.direct.ReactionAdapter import io.getstream.chat.android.client.parser2.direct.ReactionGroupAdapter import io.getstream.chat.android.client.parser2.direct.UserAdapter +import io.getstream.chat.android.client.parser2.direct.UserGroupAdapter +import io.getstream.chat.android.client.parser2.direct.UserGroupMemberAdapter import io.getstream.chat.android.models.EventType import io.getstream.chat.android.models.MessageTransformer import io.getstream.chat.android.models.UserId @@ -76,6 +78,8 @@ internal class DirectEventParser( private val userAdapter by lazy { UserAdapter(deviceAdapter, privacySettingsAdapter, dateAdapter, userTransformer) } + private val userGroupMemberAdapter by lazy { UserGroupMemberAdapter(dateAdapter) } + private val userGroupAdapter by lazy { UserGroupAdapter(userGroupMemberAdapter, dateAdapter) } private val reactionAdapter by lazy { ReactionAdapter(userAdapter, dateAdapter) } private val pollAdapter by lazy { PollAdapter(userAdapter, optionAdapter, dateAdapter, currentUserIdProvider) @@ -84,7 +88,7 @@ internal class DirectEventParser( private val messageAdapter by lazy { MessageAdapter( attachmentAdapter, channelInfoAdapter, reactionAdapter, - reactionGroupAdapter, userAdapter, moderationDetailsAdapter, moderationAdapter, + reactionGroupAdapter, userAdapter, userGroupAdapter, moderationDetailsAdapter, moderationAdapter, pollAdapter, reminderAdapter, locationAdapter, dateAdapter, messageTransformer, ) } diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/direct/MessageAdapter.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/direct/MessageAdapter.kt index 5de7137a1ae..dc75f7ce512 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/direct/MessageAdapter.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/direct/MessageAdapter.kt @@ -31,6 +31,7 @@ import io.getstream.chat.android.models.Poll import io.getstream.chat.android.models.Reaction import io.getstream.chat.android.models.ReactionGroup import io.getstream.chat.android.models.User +import io.getstream.chat.android.models.UserGroup import java.util.Date @Suppress("LongParameterList") @@ -40,6 +41,7 @@ internal class MessageAdapter( private val reactionAdapter: JsonAdapter, private val reactionGroupAdapter: ReactionGroupAdapter, private val userAdapter: JsonAdapter, + private val userGroupAdapter: JsonAdapter, private val moderationDetailsAdapter: JsonAdapter, private val moderationAdapter: JsonAdapter, private val pollAdapter: JsonAdapter, @@ -70,6 +72,10 @@ internal class MessageAdapter( var id: String? = null var latestReactions: List? = null var mentionedUsers: List? = null + var mentionedHere: Boolean? = null + var mentionedChannel: Boolean? = null + var mentionedGroups: List? = null + var mentionedRoles: List? = null var ownReactions: List? = null var parentId: String? = null var pinExpires: Date? = null @@ -120,6 +126,16 @@ internal class MessageAdapter( "id" -> id = reader.nextString() "latest_reactions" -> latestReactions = JsonParsingUtils.parseList(reader, reactionAdapter) "mentioned_users" -> mentionedUsers = JsonParsingUtils.parseList(reader, userAdapter) + "mentioned_here" -> mentionedHere = JsonParsingUtils.readNullableBoolean(reader) + "mentioned_channel" -> mentionedChannel = JsonParsingUtils.readNullableBoolean(reader) + "mentioned_groups" -> { + JsonParsingUtils.rejectExplicitNull(reader, "mentioned_groups") + mentionedGroups = JsonParsingUtils.parseList(reader, userGroupAdapter) + } + "mentioned_roles" -> { + JsonParsingUtils.rejectExplicitNull(reader, "mentioned_roles") + mentionedRoles = JsonParsingUtils.parseStringList(reader) + } "own_reactions" -> ownReactions = JsonParsingUtils.parseList(reader, reactionAdapter) "parent_id" -> parentId = JsonParsingUtils.readNullableString(reader) "pin_expires" -> pinExpires = dateAdapter.fromJson(reader) @@ -222,6 +238,10 @@ internal class MessageAdapter( id = id, latestReactions = filteredLatestReactions, mentionedUsers = mentionedUsers, + mentionedHere = mentionedHere == true, + mentionedChannel = mentionedChannel == true, + mentionedGroups = mentionedGroups.orEmpty(), + mentionedRoles = mentionedRoles.orEmpty(), ownReactions = filteredOwnReactions, parentId = parentId, pinExpires = pinExpires, diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/direct/UserGroupAdapter.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/direct/UserGroupAdapter.kt new file mode 100644 index 00000000000..f556fbd507c --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/direct/UserGroupAdapter.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.client.parser2.direct + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter +import io.getstream.chat.android.models.UserGroup +import io.getstream.chat.android.models.UserGroupMember +import java.util.Date + +internal class UserGroupAdapter( + private val memberAdapter: JsonAdapter, + private val dateAdapter: JsonAdapter, +) : JsonAdapter() { + + override fun fromJson(reader: JsonReader): UserGroup? { + if (reader.peek() == JsonReader.Token.NULL) return reader.nextNull() + + reader.beginObject() + + var id: String? = null + var name: String? = null + var description: String? = null + var teamId: String? = null + var members: List? = null + var createdBy: String? = null + var createdAt: Date? = null + var updatedAt: Date? = null + + while (reader.hasNext()) { + when (reader.nextName()) { + "id" -> id = reader.nextString() + "name" -> name = reader.nextString() + "description" -> description = JsonParsingUtils.readNullableString(reader) + "team_id" -> teamId = JsonParsingUtils.readNullableString(reader) + "members" -> members = JsonParsingUtils.parseList(reader, memberAdapter) + "created_by" -> createdBy = JsonParsingUtils.readNullableString(reader) + "created_at" -> createdAt = dateAdapter.fromJson(reader) + "updated_at" -> updatedAt = dateAdapter.fromJson(reader) + else -> reader.skipValue() + } + } + reader.endObject() + + JsonParsingUtils.requireField(id, "id", reader) + JsonParsingUtils.requireField(name, "name", reader) + + return UserGroup( + id = id, + name = name, + description = description, + team = teamId.orEmpty(), + members = members ?: emptyList(), + createdBy = createdBy, + createdAt = createdAt, + updatedAt = updatedAt, + ) + } + + override fun toJson(p0: JsonWriter, p1: UserGroup?) { + error("Serialization not supported for direct-to-domain path") + } +} diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/direct/UserGroupMemberAdapter.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/direct/UserGroupMemberAdapter.kt new file mode 100644 index 00000000000..d9738921128 --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/direct/UserGroupMemberAdapter.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.client.parser2.direct + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter +import io.getstream.chat.android.models.UserGroupMember +import java.util.Date + +internal class UserGroupMemberAdapter( + private val dateAdapter: JsonAdapter, +) : JsonAdapter() { + + override fun fromJson(reader: JsonReader): UserGroupMember? { + if (reader.peek() == JsonReader.Token.NULL) return reader.nextNull() + + reader.beginObject() + + var groupId: String? = null + var userId: String? = null + var isAdmin: Boolean? = null + var createdAt: Date? = null + + while (reader.hasNext()) { + when (reader.nextName()) { + "group_id" -> groupId = reader.nextString() + "user_id" -> userId = reader.nextString() + "is_admin" -> isAdmin = reader.nextBoolean() + "created_at" -> createdAt = dateAdapter.fromJson(reader) + else -> reader.skipValue() + } + } + reader.endObject() + + JsonParsingUtils.requireField(groupId, "group_id", reader) + JsonParsingUtils.requireField(userId, "user_id", reader) + + return UserGroupMember( + groupId = groupId, + userId = userId, + isAdmin = isAdmin ?: false, + createdAt = createdAt, + ) + } + + override fun toJson(p0: JsonWriter, p1: UserGroupMember?) { + error("Serialization not supported for direct-to-domain path") + } +} diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/MessageParsingTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/MessageParsingTest.kt index ee81bc8a796..66aaaf0645f 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/MessageParsingTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/MessageParsingTest.kt @@ -35,6 +35,8 @@ import io.getstream.chat.android.client.parser2.direct.PrivacySettingsAdapter import io.getstream.chat.android.client.parser2.direct.ReactionAdapter import io.getstream.chat.android.client.parser2.direct.ReactionGroupAdapter import io.getstream.chat.android.client.parser2.direct.UserAdapter +import io.getstream.chat.android.client.parser2.direct.UserGroupAdapter +import io.getstream.chat.android.client.parser2.direct.UserGroupMemberAdapter import io.getstream.chat.android.client.parser2.testdata.MessageTestData import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.MessageTransformer @@ -77,6 +79,13 @@ internal class MessageParsingTest { private val reactionGroupAdapter = ReactionGroupAdapter( dateAdapter = dateAdapter, ) + private val userGroupMemberAdapter = UserGroupMemberAdapter( + dateAdapter = dateAdapter, + ) + private val userGroupAdapter = UserGroupAdapter( + memberAdapter = userGroupMemberAdapter, + dateAdapter = dateAdapter, + ) private val attachmentAdapter = AttachmentAdapter() private val channelInfoAdapter = ChannelInfoAdapter() private val moderationDetailsAdapter = MessageModerationDetailsAdapter() @@ -101,6 +110,7 @@ internal class MessageParsingTest { reactionAdapter = reactionAdapter, reactionGroupAdapter = reactionGroupAdapter, userAdapter = userAdapter, + userGroupAdapter = userGroupAdapter, moderationDetailsAdapter = moderationDetailsAdapter, moderationAdapter = moderationAdapter, pollAdapter = pollAdapter, @@ -268,6 +278,14 @@ internal class MessageParsingTest { fun `Both paths - throw on explicit null thread_participants`() = assertBothPathsThrow(MessageTestData.jsonExplicitNullThreadParticipants) + @Test + fun `Both paths - throw on explicit null mentioned_groups`() = + assertBothPathsThrow(MessageTestData.jsonExplicitNullMentionedGroups) + + @Test + fun `Both paths - throw on explicit null mentioned_roles`() = + assertBothPathsThrow(MessageTestData.jsonExplicitNullMentionedRoles) + private fun assertBothPathsThrow(json: String) { assertThrows { parser.fromJson(json, DownstreamMessageDto::class.java) @@ -296,6 +314,7 @@ internal class MessageParsingTest { reactionAdapter = reactionAdapter, reactionGroupAdapter = reactionGroupAdapter, userAdapter = userAdapter, + userGroupAdapter = userGroupAdapter, moderationDetailsAdapter = moderationDetailsAdapter, moderationAdapter = moderationAdapter, pollAdapter = pollAdapter, @@ -344,6 +363,7 @@ internal class MessageParsingTest { reactionAdapter = transformedReactionAdapter, reactionGroupAdapter = reactionGroupAdapter, userAdapter = transformedUserAdapter, + userGroupAdapter = userGroupAdapter, moderationDetailsAdapter = moderationDetailsAdapter, moderationAdapter = moderationAdapter, pollAdapter = transformedPollAdapter, diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/NewMessageEventParsingTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/NewMessageEventParsingTest.kt index 0fc1f177c41..0c6eea523ec 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/NewMessageEventParsingTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/NewMessageEventParsingTest.kt @@ -37,6 +37,8 @@ import io.getstream.chat.android.client.parser2.direct.PrivacySettingsAdapter import io.getstream.chat.android.client.parser2.direct.ReactionAdapter import io.getstream.chat.android.client.parser2.direct.ReactionGroupAdapter import io.getstream.chat.android.client.parser2.direct.UserAdapter +import io.getstream.chat.android.client.parser2.direct.UserGroupAdapter +import io.getstream.chat.android.client.parser2.direct.UserGroupMemberAdapter import io.getstream.chat.android.client.parser2.testdata.NewMessageEventTestData import io.getstream.chat.android.models.NoOpChannelTransformer import io.getstream.chat.android.models.NoOpMessageTransformer @@ -77,6 +79,13 @@ internal class NewMessageEventParsingTest { private val reactionGroupAdapter = ReactionGroupAdapter( dateAdapter = dateAdapter, ) + private val userGroupMemberAdapter = UserGroupMemberAdapter( + dateAdapter = dateAdapter, + ) + private val userGroupAdapter = UserGroupAdapter( + memberAdapter = userGroupMemberAdapter, + dateAdapter = dateAdapter, + ) private val attachmentAdapter = AttachmentAdapter() private val channelInfoAdapter = ChannelInfoAdapter() private val moderationDetailsAdapter = MessageModerationDetailsAdapter() @@ -100,6 +109,7 @@ internal class NewMessageEventParsingTest { reactionAdapter = reactionAdapter, reactionGroupAdapter = reactionGroupAdapter, userAdapter = userAdapter, + userGroupAdapter = userGroupAdapter, moderationDetailsAdapter = moderationDetailsAdapter, moderationAdapter = moderationAdapter, pollAdapter = pollAdapter, diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/MessageTestData.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/MessageTestData.kt index 0d6075d7151..265e7170722 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/MessageTestData.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/MessageTestData.kt @@ -79,6 +79,28 @@ internal object MessageTestData { "mentioned_users": [ {"id": "user-2", "role": "user", "banned": false, "online": true} ], + "mentioned_here": true, + "mentioned_channel": true, + "mentioned_groups": [ + { + "id": "group-1", + "name": "engineering", + "description": "Engineering team", + "team_id": "team-1", + "members": [ + { + "group_id": "group-1", + "user_id": "user-1", + "is_admin": true, + "created_at": "2020-01-01T00:00:00.000Z" + } + ], + "created_by": "user-1", + "created_at": "2020-01-01T00:00:00.000Z", + "updated_at": "2020-01-01T00:00:00.000Z" + } + ], + "mentioned_roles": ["admin", "moderator"], "thread_participants": [ {"id": "user-3", "role": "user", "banned": false, "online": true} ], @@ -561,7 +583,11 @@ internal object MessageTestData { "command": null, "parent_id": null, "quoted_message_id": null, - "deleted_for_me": null + "deleted_for_me": null, + "mentioned_here": true, + "mentioned_channel": true, + "mentioned_groups": [], + "mentioned_roles": ["admin", "moderator"] }""" val expectedWithExplicitNulls = Message( @@ -588,6 +614,10 @@ internal object MessageTestData { deletedForMe = false, shadowed = false, showInChannel = false, + mentionedHere = true, + mentionedChannel = true, + mentionedGroups = emptyList(), + mentionedRoles = listOf("admin", "moderator"), extraData = emptyMap(), ) @@ -878,6 +908,10 @@ internal object MessageTestData { } ], "mentioned_users": [], + "mentioned_here": false, + "mentioned_channel": false, + "mentioned_groups": [], + "mentioned_roles": [], "reply_count": 0, "deleted_reply_count": 0, "created_at": "2020-01-01T00:00:00.000Z", @@ -1039,5 +1073,53 @@ internal object MessageTestData { "thread_participants": null }""" + /** + * `mentioned_groups` is non-nullable in DownstreamMessageDto (defaults to `emptyList()`). + * Same explicit-null-rejection as `thread_participants` above. + */ + @Language("JSON") + val jsonExplicitNullMentionedGroups = """{ + "id": "msg-1", + "cid": "messaging:general", + "text": "Hello", + "html": "

Hello

", + "type": "regular", + "user": {"id": "user-1", "role": "user", "banned": false, "online": true}, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "mentioned_users": [], + "reply_count": 0, + "deleted_reply_count": 0, + "created_at": "2020-01-01T00:00:00.000Z", + "updated_at": "2020-01-01T00:00:00.000Z", + "silent": false, + "mentioned_groups": null + }""" + + /** + * `mentioned_roles` is non-nullable in DownstreamMessageDto (defaults to `emptyList()`). + * Same explicit-null-rejection as `mentioned_groups` above. + */ + @Language("JSON") + val jsonExplicitNullMentionedRoles = """{ + "id": "msg-1", + "cid": "messaging:general", + "text": "Hello", + "html": "

Hello

", + "type": "regular", + "user": {"id": "user-1", "role": "user", "banned": false, "online": true}, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "mentioned_users": [], + "reply_count": 0, + "deleted_reply_count": 0, + "created_at": "2020-01-01T00:00:00.000Z", + "updated_at": "2020-01-01T00:00:00.000Z", + "silent": false, + "mentioned_roles": null + }""" + // endregion }