Skip to content
Draft
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 @@ -247,6 +247,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout
Expand Down Expand Up @@ -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
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ public fun Collection<Channel>.updateUsers(users: Map<String, User>): List<Chann
* pinnedMessages of channel instance.
*/
internal fun Channel.updateUsers(users: Map<String, User>): 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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -42,6 +44,9 @@ internal class DatabaseUserRepository(
private val userCache = LruCache<String, User>(cacheSize)
private val latestUsersFlow: MutableStateFlow<Map<String, User>> = MutableStateFlow(emptyMap())
private val dbMutex = Mutex()
private val pendingEntities = mutableListOf<UserEntity>()
private val pendingMutex = Mutex()
private var flushJob: Job? = null

override fun observeLatestUsers(): StateFlow<Map<String, User>> = latestUsersFlow

Expand All @@ -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)
}
}

Expand Down Expand Up @@ -112,10 +135,16 @@ internal class DatabaseUserRepository(
}

private fun cacheUsers(users: Collection<User>) {
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 =
Expand Down Expand Up @@ -162,5 +191,6 @@ internal class DatabaseUserRepository(

companion object {
private const val ME_ID = "me"
private const val BATCH_FLUSH_DELAY_MS = 3_000L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,9 @@ internal class EventHandlerSequential(
)
pollEvents.forEach { batchBuilder.addPollToFetch(it.poll.id) }

val users: List<User> = events.filterIsInstance<UserEvent>().map { it.user } +
val users: List<User> = events.filterIsInstance<UserEvent>()
.filterNot { it is UserStartWatchingEvent || it is UserStopWatchingEvent }
.map { it.user } +
events.filterIsInstance<HasOwnUser>().map { it.me }

batchBuilder.addUsers(users)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ internal fun RowScope.DefaultChannelItemCenterContent(
}
}

@Suppress("LongMethod")
@Composable
private fun TitleRow(
channelItemState: ItemState.ChannelItemState,
Expand All @@ -260,11 +261,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,
Expand Down Expand Up @@ -306,6 +311,7 @@ private fun TitleRow(
}
}

@Suppress("LongMethod")
@Composable
private fun MessageRow(
channelItemState: ItemState.ChannelItemState,
Expand Down Expand Up @@ -334,14 +340,22 @@ 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public fun Channels(
itemsIndexed(
items = channelItems,
key = { _, item -> item.key },
contentType = { _, item -> item::class },
) { index, item ->
itemContent(item)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -62,9 +63,11 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
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
Expand Down Expand Up @@ -234,6 +237,8 @@ public class ChannelListViewModel(
*/
private val searchMessageState: MutableStateFlow<SearchMessageState?> = MutableStateFlow(null)

private var previousChannelItems: Map<String, ItemState.ChannelItemState> = emptyMap()

private var lastNextQuery: QueryChannelsRequest? = null

/**
Expand Down Expand Up @@ -452,7 +457,9 @@ public class ChannelListViewModel(
)
}
}
}.collectLatest { newState -> channelsState = newState }
}.flowOn(Dispatchers.Default)
.debounce(CHANNEL_LIST_DEBOUNCE_MS)
.collectLatest { newState -> channelsState = newState }
}
}
}.onFailure {
Expand Down Expand Up @@ -747,12 +754,16 @@ public class ChannelListViewModel(
): List<ItemState.ChannelItemState> {
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 }
}
}

Expand All @@ -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(
Expand Down
Loading