Skip to content

Commit 3896aae

Browse files
committed
Merge branch 'v6' into feature/grouped-channels-endpoint
# Conflicts: # stream-chat-android-state/src/test/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequentialTest.kt
2 parents 02d1aab + 23b37d6 commit 3896aae

76 files changed

Lines changed: 9902 additions & 61 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ android.enableR8.fullMode=true
1010
android.suppressUnsupportedCompileSdk=34
1111

1212
# Project version
13-
version=6.37.4
13+
version=6.38.0

stream-chat-android-client/api/stream-chat-android-client.api

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ public final class io/getstream/chat/android/client/ChatClient$Builder : io/gets
283283
public final fun debugRequests (Z)Lio/getstream/chat/android/client/ChatClient$Builder;
284284
public final fun disableDistinctApiCalls ()Lio/getstream/chat/android/client/ChatClient$Builder;
285285
public final fun disableWarmUp ()Lio/getstream/chat/android/client/ChatClient$Builder;
286+
public final fun fastEventParsing (Z)Lio/getstream/chat/android/client/ChatClient$Builder;
286287
public final fun fileTransformer (Lio/getstream/chat/android/client/uploader/FileTransformer;)Lio/getstream/chat/android/client/ChatClient$Builder;
287288
public final fun fileUploader (Lio/getstream/chat/android/client/uploader/FileUploader;)Lio/getstream/chat/android/client/ChatClient$Builder;
288289
public final fun forceInsecureConnection ()Lio/getstream/chat/android/client/ChatClient$Builder;
@@ -324,11 +325,13 @@ public final class io/getstream/chat/android/client/ClientExtensionsKt {
324325
public final class io/getstream/chat/android/client/api/ChatClientConfig {
325326
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/logger/ChatLoggerConfig;ZLio/getstream/chat/android/client/notifications/handler/NotificationConfig;)V
326327
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/logger/ChatLoggerConfig;ZZLio/getstream/chat/android/client/notifications/handler/NotificationConfig;)V
327-
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/logger/ChatLoggerConfig;ZZLio/getstream/chat/android/client/notifications/handler/NotificationConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
328+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/logger/ChatLoggerConfig;ZZLio/getstream/chat/android/client/notifications/handler/NotificationConfig;Z)V
329+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/logger/ChatLoggerConfig;ZZLio/getstream/chat/android/client/notifications/handler/NotificationConfig;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
328330
public final fun getApiKey ()Ljava/lang/String;
329331
public final fun getCdnHttpUrl ()Ljava/lang/String;
330332
public final fun getDebugRequests ()Z
331333
public final fun getDistinctApiCalls ()Z
334+
public final fun getFastEventParsing ()Z
332335
public final fun getHttpUrl ()Ljava/lang/String;
333336
public final fun getLoggerConfig ()Lio/getstream/chat/android/client/logger/ChatLoggerConfig;
334337
public final fun getNotificationConfig ()Lio/getstream/chat/android/client/notifications/handler/NotificationConfig;

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4794,6 +4794,7 @@ internal constructor(
47944794
private var retryPolicy: RetryPolicy = NoRetryPolicy()
47954795
private var distinctApiCalls: Boolean = true
47964796
private var debugRequests: Boolean = false
4797+
private var fastEventParsing: Boolean = false
47974798
private var repositoryFactoryProvider: RepositoryFactory.Provider? = null
47984799
private var uploadAttachmentsNetworkType = UploadAttachmentsNetworkType.CONNECTED
47994800
private var fileTransformer: FileTransformer = NoOpFileTransformer
@@ -5060,6 +5061,22 @@ internal constructor(
50605061
this.debugRequests = shouldDebug
50615062
}
50625063

5064+
/**
5065+
* Enables the fast event-parsing path for incoming WebSocket events.
5066+
*
5067+
* When enabled, supported event types are parsed directly into domain models, bypassing
5068+
* the DTO intermediate layer. Unsupported event types fall back to the default DTO-based
5069+
* parser, so behavior is preserved for events the fast path does not yet handle.
5070+
*
5071+
* Currently supported event types:
5072+
* - `message.new`
5073+
*
5074+
* Disabled by default. The set of supported event types may grow over time.
5075+
*/
5076+
public fun fastEventParsing(enabled: Boolean): Builder = apply {
5077+
this.fastEventParsing = enabled
5078+
}
5079+
50635080
/**
50645081
* Sets a custom [RetryPolicy] used to determine whether a particular call should be retried.
50655082
* By default, no calls are retried.
@@ -5133,8 +5150,9 @@ internal constructor(
51335150
warmUp = warmUp,
51345151
loggerConfig = ChatLoggerConfigImpl(logLevel, loggerHandler),
51355152
distinctApiCalls = distinctApiCalls,
5136-
debugRequests,
5137-
notificationConfig,
5153+
debugRequests = debugRequests,
5154+
notificationConfig = notificationConfig,
5155+
fastEventParsing = fastEventParsing,
51385156
)
51395157
setupStreamLog()
51405158

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatClientConfig.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ import io.getstream.chat.android.client.notifications.handler.NotificationConfig
3434
* @param distinctApiCalls Controls whether [DistinctChatApi] is enabled or not.
3535
* @param debugRequests Controls whether requests can be recorded or not.
3636
* @param notificationConfig A notification config to be used by the client.
37+
* @param fastEventParsing Enables the fast event-parsing path for incoming WebSocket events.
38+
* When `true`, supported event types are parsed directly into domain models, bypassing the DTO
39+
* intermediate layer; unsupported event types fall back to the default DTO-based parser.
40+
* Currently supported event types: `message.new`. Disabled by default.
3741
*/
3842
@Suppress("LongParameterList")
3943
public class ChatClientConfig @JvmOverloads constructor(
@@ -46,6 +50,7 @@ public class ChatClientConfig @JvmOverloads constructor(
4650
public var distinctApiCalls: Boolean = true,
4751
public val debugRequests: Boolean,
4852
public val notificationConfig: NotificationConfig,
53+
public val fastEventParsing: Boolean = false,
4954
) {
5055
public var isAnonymous: Boolean = false
5156
}

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import io.getstream.chat.android.client.notifications.handler.NotificationConfig
7171
import io.getstream.chat.android.client.notifications.handler.NotificationHandler
7272
import io.getstream.chat.android.client.notifications.handler.NotificationHandlerFactory
7373
import io.getstream.chat.android.client.parser.ChatParser
74+
import io.getstream.chat.android.client.parser2.DirectEventParser
7475
import io.getstream.chat.android.client.parser2.MoshiChatParser
7576
import io.getstream.chat.android.client.plugins.requests.ApiRequestsAnalyser
7677
import io.getstream.chat.android.client.scope.ClientScope
@@ -164,10 +165,23 @@ constructor(
164165
}
165166
private val eventMapping by lazy { EventMapping(domainMapping) }
166167

168+
private val directEventParser: DirectEventParser? by lazy {
169+
if (config.fastEventParsing) {
170+
DirectEventParser(
171+
currentUserIdProvider = currentUserIdProvider,
172+
messageTransformer = apiModelTransformers.incomingMessageTransformer,
173+
userTransformer = apiModelTransformers.incomingUserTransformer,
174+
)
175+
} else {
176+
null
177+
}
178+
}
179+
167180
private val moshiParser: ChatParser by lazy {
168181
MoshiChatParser(
169182
eventMapping = eventMapping,
170183
dtoMapping = dtoMapping,
184+
directEventParser = directEventParser,
171185
)
172186
}
173187
private val socketFactory: SocketFactory by lazy { SocketFactory(moshiParser, tokenManager, headersUtil) }

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/ChatEvent.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.getstream.chat.android.client.events.MessageDeletedEvent
2424
import io.getstream.chat.android.client.events.MessageUpdatedEvent
2525
import io.getstream.chat.android.client.events.NewMessageEvent
2626
import io.getstream.chat.android.client.events.NotificationMessageNewEvent
27+
import io.getstream.chat.android.client.events.NotificationThreadMessageNewEvent
2728
import io.getstream.chat.android.client.events.ReactionDeletedEvent
2829
import io.getstream.chat.android.client.events.ReactionNewEvent
2930
import io.getstream.chat.android.client.events.ReactionUpdateEvent
@@ -43,5 +44,6 @@ public fun ChatEvent.enrichIfNeeded(): ChatEvent = when (this) {
4344
is ChannelTruncatedEvent -> copy(message = message?.enrichWithCid(cid))
4445
is ChannelUpdatedByUserEvent -> copy(message = message?.enrichWithCid(cid))
4546
is NotificationMessageNewEvent -> copy(message = message.enrichWithCid(cid))
47+
is NotificationThreadMessageNewEvent -> copy(message = message.enrichWithCid(cid))
4648
else -> this
4749
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.getstream.chat.android.client.parser2
18+
19+
import com.squareup.moshi.JsonAdapter
20+
import com.squareup.moshi.JsonReader
21+
import com.squareup.moshi.Moshi
22+
import io.getstream.chat.android.client.events.ChatEvent
23+
import io.getstream.chat.android.client.parser2.adapters.DateAdapter
24+
import io.getstream.chat.android.client.parser2.direct.AttachmentAdapter
25+
import io.getstream.chat.android.client.parser2.direct.ChannelInfoAdapter
26+
import io.getstream.chat.android.client.parser2.direct.DeviceAdapter
27+
import io.getstream.chat.android.client.parser2.direct.LocationAdapter
28+
import io.getstream.chat.android.client.parser2.direct.MessageAdapter
29+
import io.getstream.chat.android.client.parser2.direct.MessageModerationDetailsAdapter
30+
import io.getstream.chat.android.client.parser2.direct.MessageReminderInfoAdapter
31+
import io.getstream.chat.android.client.parser2.direct.ModerationAdapter
32+
import io.getstream.chat.android.client.parser2.direct.NewMessageEventAdapter
33+
import io.getstream.chat.android.client.parser2.direct.OptionAdapter
34+
import io.getstream.chat.android.client.parser2.direct.PollAdapter
35+
import io.getstream.chat.android.client.parser2.direct.PrivacySettingsAdapter
36+
import io.getstream.chat.android.client.parser2.direct.ReactionAdapter
37+
import io.getstream.chat.android.client.parser2.direct.ReactionGroupAdapter
38+
import io.getstream.chat.android.client.parser2.direct.UserAdapter
39+
import io.getstream.chat.android.models.EventType
40+
import io.getstream.chat.android.models.MessageTransformer
41+
import io.getstream.chat.android.models.UserId
42+
import io.getstream.chat.android.models.UserTransformer
43+
import io.getstream.log.taggedLogger
44+
import okio.Buffer
45+
import java.util.Date
46+
47+
/**
48+
* Routes incoming JSON events to hand-written [JsonAdapter]s that parse directly into domain models,
49+
* bypassing the DTO intermediate layer. Returns `null` for unsupported event types so the caller
50+
* can fall back to the existing DTO path.
51+
*/
52+
internal class DirectEventParser(
53+
private val currentUserIdProvider: () -> UserId?,
54+
private val messageTransformer: MessageTransformer,
55+
private val userTransformer: UserTransformer,
56+
) {
57+
58+
// region Leaf adapters
59+
60+
private val moshi by lazy { Moshi.Builder().add(Date::class.java, DateAdapter()).build() }
61+
private val dateAdapter by lazy { moshi.adapter(Date::class.java) }
62+
private val deviceAdapter by lazy { DeviceAdapter() }
63+
private val privacySettingsAdapter by lazy { PrivacySettingsAdapter() }
64+
private val attachmentAdapter by lazy { AttachmentAdapter() }
65+
private val channelInfoAdapter by lazy { ChannelInfoAdapter() }
66+
private val moderationDetailsAdapter by lazy { MessageModerationDetailsAdapter() }
67+
private val moderationAdapter by lazy { ModerationAdapter() }
68+
private val optionAdapter by lazy { OptionAdapter() }
69+
private val locationAdapter by lazy { LocationAdapter(dateAdapter) }
70+
private val reactionGroupAdapter by lazy { ReactionGroupAdapter(dateAdapter) }
71+
72+
// endregion
73+
74+
// region Composed adapters
75+
76+
private val userAdapter by lazy {
77+
UserAdapter(deviceAdapter, privacySettingsAdapter, dateAdapter, userTransformer)
78+
}
79+
private val reactionAdapter by lazy { ReactionAdapter(userAdapter, dateAdapter) }
80+
private val pollAdapter by lazy {
81+
PollAdapter(userAdapter, optionAdapter, dateAdapter, currentUserIdProvider)
82+
}
83+
private val reminderAdapter by lazy { MessageReminderInfoAdapter(dateAdapter) }
84+
private val messageAdapter by lazy {
85+
MessageAdapter(
86+
attachmentAdapter, channelInfoAdapter, reactionAdapter,
87+
reactionGroupAdapter, userAdapter, moderationDetailsAdapter, moderationAdapter,
88+
pollAdapter, reminderAdapter, locationAdapter, dateAdapter, messageTransformer,
89+
)
90+
}
91+
92+
// endregion
93+
94+
// region Event adapters
95+
96+
private val newMessageEventAdapter by lazy {
97+
NewMessageEventAdapter(messageAdapter, userAdapter)
98+
}
99+
100+
// endregion
101+
102+
/** Registry mapping event type strings to their direct adapters. */
103+
private val adapterMap: Map<String, JsonAdapter<out ChatEvent>> by lazy {
104+
mapOf(EventType.MESSAGE_NEW to newMessageEventAdapter)
105+
}
106+
107+
/**
108+
* Attempts to parse [raw] JSON into a [ChatEvent] using a direct adapter.
109+
* Returns `null` if the event type is not supported by any direct adapter.
110+
*/
111+
fun parse(raw: String): ChatEvent? {
112+
val type = extractType(raw) ?: return null
113+
val adapter = adapterMap[type] ?: return null
114+
return adapter.fromJson(raw)
115+
}
116+
117+
companion object {
118+
119+
private val logger by taggedLogger("DirectEventParser")
120+
121+
/**
122+
* Extracts the `"type"` field value from the top level of a JSON object
123+
* using a streaming [JsonReader]. Stops as soon as the field is found.
124+
*/
125+
@Suppress("NestedBlockDepth")
126+
internal fun extractType(raw: String): String? {
127+
if (raw.isBlank()) return null
128+
val reader = JsonReader.of(Buffer().writeUtf8(raw))
129+
return try {
130+
reader.use {
131+
if (it.peek() != JsonReader.Token.BEGIN_OBJECT) return null
132+
it.beginObject()
133+
while (it.hasNext()) {
134+
if (it.nextName() == "type") {
135+
return if (it.peek() == JsonReader.Token.NULL) {
136+
it.nextNull<String>()
137+
} else {
138+
it.nextString()
139+
}
140+
} else {
141+
it.skipValue()
142+
}
143+
}
144+
null
145+
}
146+
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
147+
logger.v { "extractType failed; falling back to DTO path: ${e.message}" }
148+
null
149+
}
150+
}
151+
}
152+
}

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/MoshiChatParser.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import retrofit2.converter.moshi.MoshiConverterFactory
5959
internal class MoshiChatParser(
6060
private val eventMapping: EventMapping,
6161
private val dtoMapping: DtoMapping,
62+
private val directEventParser: DirectEventParser?,
6263
) : ChatParser {
6364

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

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

144-
private fun parseAndProcessEvent(raw: String): ChatEvent = with(eventMapping) {
145-
val event = chatEventDtoAdapter.fromJson(raw)!!.toDomain()
146-
return event.enrichIfNeeded()
145+
private fun parseAndProcessEvent(raw: String): ChatEvent {
146+
val directEvent = directEventParser?.parse(raw)
147+
if (directEvent != null) {
148+
// Direct adapters handle enrichment inline — no enrichIfNeeded() needed.
149+
return directEvent
150+
}
151+
return with(eventMapping) { chatEventDtoAdapter.fromJson(raw)!!.toDomain() }.enrichIfNeeded()
147152
}
148153
}

0 commit comments

Comments
 (0)