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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import io.getstream.chat.android.client.notifications.handler.NotificationConfig
import io.getstream.chat.android.client.notifications.handler.NotificationHandler
import io.getstream.chat.android.client.notifications.handler.NotificationHandlerFactory
import io.getstream.chat.android.client.parser.ChatParser
import io.getstream.chat.android.client.parser2.DirectEventParser
import io.getstream.chat.android.client.parser2.MoshiChatParser
import io.getstream.chat.android.client.plugins.requests.ApiRequestsAnalyser
import io.getstream.chat.android.client.scope.ClientScope
Expand Down Expand Up @@ -164,10 +165,19 @@ constructor(
}
private val eventMapping by lazy { EventMapping(domainMapping) }

private val directEventParser by lazy {
DirectEventParser(
currentUserIdProvider = currentUserIdProvider,
messageTransformer = apiModelTransformers.incomingMessageTransformer,
userTransformer = apiModelTransformers.incomingUserTransformer,
)
}

private val moshiParser: ChatParser by lazy {
MoshiChatParser(
eventMapping = eventMapping,
dtoMapping = dtoMapping,
directEventParser = directEventParser,
)
}
private val socketFactory: SocketFactory by lazy { SocketFactory(moshiParser, tokenManager, headersUtil) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import io.getstream.chat.android.client.events.MessageDeletedEvent
import io.getstream.chat.android.client.events.MessageUpdatedEvent
import io.getstream.chat.android.client.events.NewMessageEvent
import io.getstream.chat.android.client.events.NotificationMessageNewEvent
import io.getstream.chat.android.client.events.NotificationThreadMessageNewEvent
import io.getstream.chat.android.client.events.ReactionDeletedEvent
import io.getstream.chat.android.client.events.ReactionNewEvent
import io.getstream.chat.android.client.events.ReactionUpdateEvent
Expand All @@ -43,5 +44,6 @@ public fun ChatEvent.enrichIfNeeded(): ChatEvent = when (this) {
is ChannelTruncatedEvent -> copy(message = message?.enrichWithCid(cid))
is ChannelUpdatedByUserEvent -> copy(message = message?.enrichWithCid(cid))
is NotificationMessageNewEvent -> copy(message = message.enrichWithCid(cid))
is NotificationThreadMessageNewEvent -> copy(message = message.enrichWithCid(cid))
else -> this
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.client.parser2

import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.Moshi
import io.getstream.chat.android.client.events.ChatEvent
import io.getstream.chat.android.client.parser2.adapters.DateAdapter
import io.getstream.chat.android.client.parser2.direct.AttachmentAdapter
import io.getstream.chat.android.client.parser2.direct.ChannelInfoAdapter
import io.getstream.chat.android.client.parser2.direct.DeviceAdapter
import io.getstream.chat.android.client.parser2.direct.LocationAdapter
import io.getstream.chat.android.client.parser2.direct.MessageAdapter
import io.getstream.chat.android.client.parser2.direct.MessageModerationDetailsAdapter
import io.getstream.chat.android.client.parser2.direct.MessageReminderInfoAdapter
import io.getstream.chat.android.client.parser2.direct.ModerationAdapter
import io.getstream.chat.android.client.parser2.direct.NewMessageEventAdapter
import io.getstream.chat.android.client.parser2.direct.OptionAdapter
import io.getstream.chat.android.client.parser2.direct.PollAdapter
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.models.EventType
import io.getstream.chat.android.models.MessageTransformer
import io.getstream.chat.android.models.UserId
import io.getstream.chat.android.models.UserTransformer
import okio.Buffer
import java.util.Date

/**
* Routes incoming JSON events to hand-written [JsonAdapter]s that parse directly into domain models,
* bypassing the DTO intermediate layer. Returns `null` for unsupported event types so the caller
* can fall back to the existing DTO path.
*/
internal class DirectEventParser(
private val currentUserIdProvider: () -> UserId?,
private val messageTransformer: MessageTransformer,
private val userTransformer: UserTransformer,
) {

// region Leaf adapters

private val moshi by lazy { Moshi.Builder().add(Date::class.java, DateAdapter()).build() }
private val dateAdapter by lazy { moshi.adapter(Date::class.java) }
private val deviceAdapter by lazy { DeviceAdapter() }
private val privacySettingsAdapter by lazy { PrivacySettingsAdapter() }
private val attachmentAdapter by lazy { AttachmentAdapter() }
private val channelInfoAdapter by lazy { ChannelInfoAdapter() }
private val moderationDetailsAdapter by lazy { MessageModerationDetailsAdapter() }
private val moderationAdapter by lazy { ModerationAdapter() }
private val optionAdapter by lazy { OptionAdapter() }
private val locationAdapter by lazy { LocationAdapter(dateAdapter) }
private val reactionGroupAdapter by lazy { ReactionGroupAdapter(dateAdapter) }

// endregion

// region Composed adapters

private val userAdapter by lazy {
UserAdapter(deviceAdapter, privacySettingsAdapter, dateAdapter, userTransformer)
}
private val reactionAdapter by lazy { ReactionAdapter(userAdapter, dateAdapter) }
private val pollAdapter by lazy {
PollAdapter(userAdapter, optionAdapter, dateAdapter, currentUserIdProvider)
}
private val reminderAdapter by lazy { MessageReminderInfoAdapter(dateAdapter) }
private val messageAdapter by lazy {
MessageAdapter(
attachmentAdapter, channelInfoAdapter, reactionAdapter,
reactionGroupAdapter, userAdapter, moderationDetailsAdapter, moderationAdapter,
pollAdapter, reminderAdapter, locationAdapter, dateAdapter, messageTransformer,
)
}

// endregion

// region Event adapters

private val newMessageEventAdapter by lazy {
NewMessageEventAdapter(messageAdapter, userAdapter)
}

// endregion

/** Registry mapping event type strings to their direct adapters. */
private val adapterMap: Map<String, JsonAdapter<out ChatEvent>> by lazy {
mapOf(EventType.MESSAGE_NEW to newMessageEventAdapter)
}

/**
* Attempts to parse [raw] JSON into a [ChatEvent] using a direct adapter.
* Returns `null` if the event type is not supported by any direct adapter.
*/
fun parse(raw: String): ChatEvent? {
val type = extractType(raw) ?: return null
val adapter = adapterMap[type] ?: return null
return adapter.fromJson(raw)
}

companion object {
/**
* Extracts the `"type"` field value from the top level of a JSON object
* using a streaming [JsonReader]. Stops as soon as the field is found.
*/
@Suppress("NestedBlockDepth")
internal fun extractType(raw: String): String? {
if (raw.isBlank()) return null
val reader = JsonReader.of(Buffer().writeUtf8(raw))
return try {
reader.use {
if (it.peek() != JsonReader.Token.BEGIN_OBJECT) return null
it.beginObject()
while (it.hasNext()) {
if (it.nextName() == "type") {
return if (it.peek() == JsonReader.Token.NULL) {
it.nextNull<String>()
} else {
it.nextString()
}
} else {
it.skipValue()
}
}
null
}
} catch (@Suppress("TooGenericExceptionCaught", "SwallowedException") _: Exception) {
null
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import retrofit2.converter.moshi.MoshiConverterFactory
internal class MoshiChatParser(
private val eventMapping: EventMapping,
private val dtoMapping: DtoMapping,
private val directEventParser: DirectEventParser,
) : ChatParser {

private val moshi: Moshi by lazy {
Expand Down Expand Up @@ -141,8 +142,12 @@ internal class MoshiChatParser(

private val chatEventDtoAdapter = moshi.adapter(ChatEventDto::class.java)

private fun parseAndProcessEvent(raw: String): ChatEvent = with(eventMapping) {
val event = chatEventDtoAdapter.fromJson(raw)!!.toDomain()
return event.enrichIfNeeded()
private fun parseAndProcessEvent(raw: String): ChatEvent {
val directEvent = directEventParser.parse(raw)
if (directEvent != null) {
// Direct adapters handle enrichment inline — no enrichIfNeeded() needed.
return directEvent
}
return with(eventMapping) { chatEventDtoAdapter.fromJson(raw)!!.toDomain() }.enrichIfNeeded()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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.Attachment

internal class AttachmentAdapter : JsonAdapter<Attachment>() {

@Suppress("LongMethod")
override fun fromJson(reader: JsonReader): Attachment? {
if (reader.peek() == JsonReader.Token.NULL) return reader.nextNull()

reader.beginObject()
var assetUrl: String? = null
var authorName: String? = null
var authorLink: String? = null
var fallback: String? = null
var fileSize: Int = 0
var image: String? = null
var imageUrl: String? = null
var mimeType: String? = null
var name: String? = null
var ogScrapeUrl: String? = null
var text: String? = null
var thumbUrl: String? = null
var title: String? = null
var titleLink: String? = null
var type: String? = null
var originalHeight: Int? = null
var originalWidth: Int? = null
var extraData: MutableMap<String, Any>? = null

while (reader.hasNext()) {
val key = reader.nextName()
when (key) {
"asset_url" -> assetUrl = JsonParsingUtils.readNullableString(reader)
"author_name" -> authorName = JsonParsingUtils.readNullableString(reader)
"author_link" -> authorLink = JsonParsingUtils.readNullableString(reader)
"fallback" -> fallback = JsonParsingUtils.readNullableString(reader)
"file_size" -> fileSize = reader.nextInt()
"image" -> image = JsonParsingUtils.readNullableString(reader)
"image_url" -> imageUrl = JsonParsingUtils.readNullableString(reader)
"mime_type" -> mimeType = JsonParsingUtils.readNullableString(reader)
"name" -> name = JsonParsingUtils.readNullableString(reader)
"og_scrape_url" -> ogScrapeUrl = JsonParsingUtils.readNullableString(reader)
"text" -> text = JsonParsingUtils.readNullableString(reader)
"thumb_url" -> thumbUrl = JsonParsingUtils.readNullableString(reader)
"title" -> title = JsonParsingUtils.readNullableString(reader)
"title_link" -> titleLink = JsonParsingUtils.readNullableString(reader)
"type" -> type = JsonParsingUtils.readNullableString(reader)
"original_height" -> originalHeight = JsonParsingUtils.readNullableInt(reader)
"original_width" -> originalWidth = JsonParsingUtils.readNullableInt(reader)
else -> extraData = JsonParsingUtils.accumulateExtraData(key, reader, extraData)
}
}
reader.endObject()

return Attachment(
assetUrl = assetUrl,
authorName = authorName,
authorLink = authorLink,
fallback = fallback,
fileSize = fileSize,
image = image,
imageUrl = imageUrl,
mimeType = mimeType,
name = name,
ogUrl = ogScrapeUrl,
text = text,
thumbUrl = thumbUrl,
title = title,
titleLink = titleLink,
type = type,
originalHeight = originalHeight,
originalWidth = originalWidth,
extraData = extraData?.toMutableMap() ?: mutableMapOf(),
)
}

override fun toJson(p0: JsonWriter, p1: Attachment?) {
error("Serialization not supported for direct-to-domain path")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

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

internal class ChannelInfoAdapter : JsonAdapter<ChannelInfo>() {
override fun fromJson(reader: JsonReader): ChannelInfo? {
if (reader.peek() == JsonReader.Token.NULL) return reader.nextNull()

reader.beginObject()
var cid: String? = null
var id: String? = null
var memberCount: Int = 0
var name: String? = null
var type: String? = null
var image: String? = null

while (reader.hasNext()) {
when (reader.nextName()) {
"cid" -> cid = JsonParsingUtils.readNullableString(reader)
"id" -> id = JsonParsingUtils.readNullableString(reader)
"member_count" -> memberCount = reader.nextInt()
"name" -> name = JsonParsingUtils.readNullableString(reader)
"type" -> type = JsonParsingUtils.readNullableString(reader)
"image" -> image = JsonParsingUtils.readNullableString(reader)
else -> reader.skipValue()
}
}
reader.endObject()

return ChannelInfo(
cid = cid,
id = id,
memberCount = memberCount,
name = name,
type = type,
image = image,
)
}

override fun toJson(p0: JsonWriter, p1: ChannelInfo?) {
error("Serialization not supported for direct-to-domain path")
}
}
Loading
Loading