From 7fd8e36794797fb17d7df35f67c858d4c40e4ee4 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 27 Feb 2026 13:04:39 +0100 Subject: [PATCH 1/6] perf(channels): eliminate startup Davey and reduce scroll jank MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Thread.sleep busy-wait in awaitInitializationState with runBlocking + coroutine suspension — wakes instantly on state change instead of polling every 100ms (eliminates ~2.9s Davey on cold start). Move channel list combine pipeline off Main via flowOn(Default), add 100ms debounce to collapse rapid WebSocket event floods, and reuse ChannelItemState instances by CID to let Compose skip recomposition for unchanged items. --- .../chat/android/client/ChatClient.kt | 16 ++++++-------- .../channels/ChannelListViewModel.kt | 21 +++++++++++++++++-- 2 files changed, 25 insertions(+), 12 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 2caef41a0b3..7221c5e3e19 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 @@ -246,6 +246,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.job import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.plus import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -422,18 +423,13 @@ internal constructor( "This method is used to avoid race-condition between plugin initialization and dependency resolution.", ) public fun awaitInitializationState(timeoutMilliseconds: Long): InitializationState? { - var initState: InitializationState? = clientState.initializationState.value - var spendTime = 0L - inheritScope { Job(it) }.launch { - initState = withTimeoutOrNull(timeoutMilliseconds) { - clientState.initializationState.first { it == InitializationState.COMPLETE } + val currentState = clientState.initializationState.value + if (currentState != InitializationState.INITIALIZING) return currentState + return runBlocking { + withTimeoutOrNull(timeoutMilliseconds) { + clientState.initializationState.first { it != InitializationState.INITIALIZING } } } - while (initState == InitializationState.INITIALIZING && spendTime < timeoutMilliseconds) { - java.lang.Thread.sleep(INITIALIZATION_DELAY) - spendTime += INITIALIZATION_DELAY - } - return initState } /** diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt index 222af8848d1..cebee0587e3 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt @@ -60,8 +60,11 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest @@ -234,6 +237,8 @@ public class ChannelListViewModel( */ private val searchMessageState: MutableStateFlow = MutableStateFlow(null) + private var previousChannelItems: Map = emptyMap() + private var lastNextQuery: QueryChannelsRequest? = null /** @@ -452,7 +457,9 @@ public class ChannelListViewModel( ) } } - }.collectLatest { newState -> channelsState = newState } + }.flowOn(Dispatchers.Default) + .debounce(CHANNEL_LIST_DEBOUNCE_MS) + .collectLatest { newState -> channelsState = newState } } } }.onFailure { @@ -747,12 +754,16 @@ public class ChannelListViewModel( ): List { val mutedChannelIds = channelMutes.map { channelMute -> channelMute.channel?.cid }.toSet() return channels.map { - ItemState.ChannelItemState( + val newItem = ItemState.ChannelItemState( channel = it, isMuted = it.cid in mutedChannelIds, typingUsers = typingEvents[it.cid]?.users ?: emptyList(), draftMessage = draftMessages[it.cid], ) + val previous = previousChannelItems[newItem.key] + if (previous != null && previous == newItem) previous else newItem + }.also { items -> + previousChannelItems = items.associateBy { it.key } } } @@ -771,6 +782,12 @@ public class ChannelListViewModel( * Minimum length of the search query to start searching for channels. */ private const val MIN_CHANNEL_SEARCH_QUERY_LENGTH = 3 + + /** + * Debounce for channel list state updates. Collapses rapid WebSocket events + * (e.g. 30 user.watching.start events) into fewer UI updates. + */ + private const val CHANNEL_LIST_DEBOUNCE_MS = 100L } private data class SearchMessageState( From 1bb8b86b375f7627ae0bdb093c708f3a9d4fcadb Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 27 Feb 2026 13:27:03 +0100 Subject: [PATCH 2/6] perf(compose): reduce recompositions during scroll Memoize channel name and message preview with remember(). Wrap Timestamp formatting in remember(date, formatType). Add contentType to LazyList for better item reuse. Replace collectAsState() with direct .value access for stable user state in lambda blocks. Remove duplicate Crossfade from Avatar (Coil already handles crossfade). --- .../compose/ui/channels/list/ChannelItem.kt | 16 ++++++++++---- .../compose/ui/channels/list/ChannelList.kt | 6 ++---- .../compose/ui/channels/list/Channels.kt | 1 + .../compose/ui/components/Timestamp.kt | 11 ++++++---- .../compose/ui/components/avatar/Avatar.kt | 21 ++++++++----------- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt index 6dd08ebb50c..498c8448379 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt @@ -260,11 +260,15 @@ private fun TitleRow( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), ) { + val channelNameFormatter = ChatTheme.channelNameFormatter + val formattedChannelName = remember(channel, currentUser, channelNameFormatter) { + channelNameFormatter.formatChannelName(channel, currentUser) + } Text( modifier = Modifier .testTag("Stream_ChannelName") .weight(1f, fill = false), - text = ChatTheme.channelNameFormatter.formatChannelName(channel, currentUser), + text = formattedChannelName, style = ChatTheme.typography.headingSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -334,14 +338,18 @@ private fun MessageRow( ) } - val lastMessageText = + val messagePreviewFormatter = ChatTheme.messagePreviewFormatter + val lastMessageText = remember( + channelItemState.draftMessage, lastMessage, currentUser, isDirectMessaging, messagePreviewFormatter, + ) { channelItemState.draftMessage - ?.let { ChatTheme.messagePreviewFormatter.formatDraftMessagePreview(it) } + ?.let { messagePreviewFormatter.formatDraftMessagePreview(it) } ?: lastMessage?.let { - ChatTheme.messagePreviewFormatter.formatMessagePreview( + messagePreviewFormatter.formatMessagePreview( it, currentUser, isDirectMessaging, ) } + } Text( modifier = Modifier diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt index e346c37dd9b..b3a2162dc03 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt @@ -140,7 +140,6 @@ public fun ChannelList( } }, channelContent: @Composable LazyItemScope.(ItemState.ChannelItemState) -> Unit = { itemState -> - val user by viewModel.user.collectAsState() val selectedCid = viewModel.selectedChannel.value?.cid val enrichedItemState = if (selectedCid != null && itemState.channel.cid == selectedCid) { itemState.copy(isSelected = true) @@ -150,18 +149,17 @@ public fun ChannelList( with(ChatTheme.componentFactory) { ChannelListItemContent( channelItem = enrichedItemState, - currentUser = user, + currentUser = viewModel.user.value, onChannelClick = onChannelClick, onChannelLongClick = onChannelLongClick, ) } }, searchResultContent: @Composable LazyItemScope.(ItemState.SearchResultItemState) -> Unit = { itemState -> - val user by viewModel.user.collectAsState() with(ChatTheme.componentFactory) { SearchResultItemContent( searchResultItem = itemState, - currentUser = user, + currentUser = viewModel.user.value, onSearchResultClick = onSearchResultClick, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/Channels.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/Channels.kt index 9236fd81bae..a80de917852 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/Channels.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/Channels.kt @@ -90,6 +90,7 @@ public fun Channels( itemsIndexed( items = channelItems, key = { _, item -> item.key }, + contentType = { _, item -> item::class }, ) { index, item -> itemContent(item) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/Timestamp.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/Timestamp.kt index 2efd46ad7da..7c60c791f99 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/Timestamp.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/Timestamp.kt @@ -18,6 +18,7 @@ package io.getstream.chat.android.compose.ui.components import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag @@ -54,10 +55,12 @@ public fun Timestamp( val timestamp = if (LocalInspectionMode.current) { "13:49" } else { - when (formatType) { - TIME -> formatter.formatTime(date) - DATE -> formatter.formatDate(date) - RELATIVE -> formatter.formatRelativeTime(date) + remember(date, formatType) { + when (formatType) { + TIME -> formatter.formatTime(date) + DATE -> formatter.formatDate(date) + RELATIVE -> formatter.formatRelativeTime(date) + } } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/Avatar.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/Avatar.kt index 64ce794eda5..339c0a00fdd 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/Avatar.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/avatar/Avatar.kt @@ -16,7 +16,6 @@ package io.getstream.chat.android.compose.ui.components.avatar -import androidx.compose.animation.Crossfade import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -55,17 +54,15 @@ internal fun Avatar( content = { state -> val painter = (state as? AsyncImagePainter.State.Success)?.painter - Crossfade(targetState = painter) { painter -> - if (painter == null) { - fallback() - } else { - Image( - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - painter = painter, - contentDescription = null, - ) - } + if (painter == null) { + fallback() + } else { + Image( + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + painter = painter, + contentDescription = null, + ) } }, ) From ec586516597fed7b65b101f9752bccc476ceac99 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 27 Feb 2026 14:05:55 +0100 Subject: [PATCH 3/6] perf(offline): batch user DB writes with 3s collection window Replace fire-and-forget per-call insertMany with a batched flush. Cache updates remain immediate; only Room writes are deferred. Entities are deduplicated by user ID (last-write-wins) at flush time. --- .../user/internal/DatabaseUserRepository.kt | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt index c64b936404f..78b51b32410 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt @@ -25,6 +25,8 @@ import io.getstream.chat.android.models.Mute import io.getstream.chat.android.models.User import io.getstream.log.taggedLogger import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -42,6 +44,9 @@ internal class DatabaseUserRepository( private val userCache = LruCache(cacheSize) private val latestUsersFlow: MutableStateFlow> = MutableStateFlow(emptyMap()) private val dbMutex = Mutex() + private val pendingEntities = mutableListOf() + private val pendingMutex = Mutex() + private var flushJob: Job? = null override fun observeLatestUsers(): StateFlow> = latestUsersFlow @@ -62,11 +67,29 @@ internal class DatabaseUserRepository( .filter { it != userCache[it.id] } .map { it.toEntity() } cacheUsers(users) - scope.launchWithMutex(dbMutex) { - logger.v { "[insertUsers] inserting ${usersToInsert.size} entities on DB, updated ${users.size} on cache" } - usersToInsert - .takeUnless { it.isEmpty() } - ?.let { userDao.insertMany(it) } + if (usersToInsert.isEmpty()) return + pendingMutex.withLock { + pendingEntities.addAll(usersToInsert) + if (flushJob?.isActive != true) { + flushJob = scope.launch { + delay(BATCH_FLUSH_DELAY_MS) + flushPendingUsers() + } + } + } + } + + private suspend fun flushPendingUsers() { + val snapshot = pendingMutex.withLock { + val copy = pendingEntities.toList() + pendingEntities.clear() + copy + } + if (snapshot.isEmpty()) return + val deduped = snapshot.associateBy { it.id }.values.toList() + logger.v { "[insertUsers] batch flushing ${deduped.size} entities to DB (from ${snapshot.size} enqueued)" } + dbMutex.withLock { + userDao.insertMany(deduped) } } @@ -162,5 +185,6 @@ internal class DatabaseUserRepository( companion object { private const val ME_ID = "me" + private const val BATCH_FLUSH_DELAY_MS = 3_000L } } From ae8dce223e44be74e46993664b9ca77d109c016b Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 27 Feb 2026 14:23:39 +0100 Subject: [PATCH 4/6] style: apply spotless formatting --- .../java/io/getstream/chat/android/client/ChatClient.kt | 2 +- .../chat/android/compose/ui/channels/list/ChannelItem.kt | 6 +++++- .../compose/viewmodel/channels/ChannelListViewModel.kt | 4 ++-- 3 files changed, 8 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 7221c5e3e19..4dbb89f7689 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 @@ -246,8 +246,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.job import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.plus +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withTimeout diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt index 498c8448379..401f2706da8 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt @@ -340,7 +340,11 @@ private fun MessageRow( val messagePreviewFormatter = ChatTheme.messagePreviewFormatter val lastMessageText = remember( - channelItemState.draftMessage, lastMessage, currentUser, isDirectMessaging, messagePreviewFormatter, + channelItemState.draftMessage, + lastMessage, + currentUser, + isDirectMessaging, + messagePreviewFormatter, ) { channelItemState.draftMessage ?.let { messagePreviewFormatter.formatDraftMessagePreview(it) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt index cebee0587e3..4aae569d930 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt @@ -53,6 +53,7 @@ import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction import io.getstream.chat.android.ui.common.utils.extensions.defaultChannelListFilter import io.getstream.log.taggedLogger import io.getstream.result.call.toUnitCall +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren @@ -60,14 +61,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.job From adf3d9142f29c93ffcb5ce2ccf3c8229395d1f1a Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 27 Feb 2026 14:26:32 +0100 Subject: [PATCH 5/6] fix(detekt): suppress LongMethod on TitleRow and MessageRow --- .../chat/android/compose/ui/channels/list/ChannelItem.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt index 401f2706da8..52d1faa8461 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt @@ -240,6 +240,7 @@ internal fun RowScope.DefaultChannelItemCenterContent( } } +@Suppress("LongMethod") @Composable private fun TitleRow( channelItemState: ItemState.ChannelItemState, @@ -310,6 +311,7 @@ private fun TitleRow( } } +@Suppress("LongMethod") @Composable private fun MessageRow( channelItemState: ItemState.ChannelItemState, From 1bc16f5e449a6d428e4b1a2af5c8881b23a3b611 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 27 Feb 2026 15:26:26 +0100 Subject: [PATCH 6/6] perf(channels): skip redundant object allocation on watcher events Channel.updateUsers checks structural equality before copy, cacheUsers skips emission when data unchanged, and watcher events are filtered from user extraction pipeline. --- .../android/client/extensions/internal/Channel.kt | 2 +- .../domain/user/internal/DatabaseUserRepository.kt | 12 +++++++++--- .../event/handler/internal/EventHandlerSequential.kt | 4 +++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Channel.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Channel.kt index 5f05786ded0..0285bc99651 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Channel.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Channel.kt @@ -265,7 +265,7 @@ public fun Collection.updateUsers(users: Map): List): Channel { - return if (users().map(User::id).any(users::containsKey)) { + return if (users().any { user -> users[user.id]?.let { it != user } == true }) { copy( messages = messages.updateUsers(users), members = members.updateUsers(users).toList(), diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt index 78b51b32410..3553d3b0594 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt @@ -135,10 +135,16 @@ internal class DatabaseUserRepository( } private fun cacheUsers(users: Collection) { - for (userEntity in users) { - userCache.put(userEntity.id, userEntity) + var changed = false + for (user in users) { + if (userCache[user.id] != user) { + userCache.put(user.id, user) + changed = true + } + } + if (changed) { + scope.launch { latestUsersFlow.value = userCache.snapshot() } } - scope.launch { latestUsersFlow.value = userCache.snapshot() } } private fun User.toEntity(): UserEntity = diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/internal/EventHandlerSequential.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/internal/EventHandlerSequential.kt index fdcb3de116e..934c1fdf89b 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/internal/EventHandlerSequential.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/internal/EventHandlerSequential.kt @@ -526,7 +526,9 @@ internal class EventHandlerSequential( ) pollEvents.forEach { batchBuilder.addPollToFetch(it.poll.id) } - val users: List = events.filterIsInstance().map { it.user } + + val users: List = events.filterIsInstance() + .filterNot { it is UserStartWatchingEvent || it is UserStopWatchingEvent } + .map { it.user } + events.filterIsInstance().map { it.me } batchBuilder.addUsers(users)