optimize: optimize chatList

This commit is contained in:
k1ngsterr1
2026-02-08 07:07:43 +05:00
parent 162747ea35
commit 58b754d5ba
3 changed files with 76 additions and 46 deletions

View File

@@ -272,7 +272,6 @@ android.util.Log.d("ChatsListScreen", "✅ Total LaunchedEffect: ${System.curren
// Status dialog state
var showStatusDialog by remember { mutableStateOf(false) }
val debugLogs by ProtocolManager.debugLogs.collectAsState()
// 📬 Requests screen state
var showRequestsScreen by remember { mutableStateOf(false) }
@@ -286,8 +285,8 @@ android.util.Log.d("ChatsListScreen", "✅ Total LaunchedEffect: ${System.curren
var dialogToBlock by remember { mutableStateOf<DialogUiModel?>(null) }
var dialogToUnblock by remember { mutableStateOf<DialogUiModel?>(null) }
// Trigger для обновления статуса блокировки
var blocklistUpdateTrigger by remember { mutableStateOf(0) }
// Реактивный set заблокированных пользователей из ViewModel (Room Flow)
val blockedUsers by chatsViewModel.blockedUsers.collectAsState()
// Dev console dialog - commented out for now
/*
@@ -956,10 +955,13 @@ android.util.Log.d("ChatsListScreen", "✅ Total LaunchedEffect: ${System.curren
)
} else {
// Show dialogs list
val dividerColor =
val dividerColor = remember(isDarkTheme) {
if (isDarkTheme) Color(0xFF3A3A3A)
else Color(0xFFE8E8E8)
val listBackgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7)
}
val listBackgroundColor = remember(isDarkTheme) {
if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7)
}
// 🔥 Берем dialogs из chatsState для
// консистентности
// 📌 Сортируем: pinned сначала, потом по времени
@@ -1011,26 +1013,15 @@ android.util.Log.d("ChatsListScreen", "✅ Total LaunchedEffect: ${System.curren
items(
currentDialogs,
key = { it.opponentKey }
key = { it.opponentKey },
contentType = { "dialog" }
) { dialog ->
val isSavedMessages =
dialog.opponentKey ==
accountPublicKey
// Check if user is blocked
var isBlocked by remember {
mutableStateOf(
false
)
}
LaunchedEffect(
dialog.opponentKey,
blocklistUpdateTrigger
) {
isBlocked =
chatsViewModel
.isUserBlocked(
dialog.opponentKey
)
val isBlocked = blockedUsers.contains(dialog.opponentKey)
val isTyping by remember(dialog.opponentKey) {
derivedStateOf { typingUsers.contains(dialog.opponentKey) }
}
Column {
@@ -1040,10 +1031,7 @@ android.util.Log.d("ChatsListScreen", "✅ Total LaunchedEffect: ${System.curren
isDarkTheme =
isDarkTheme,
isTyping =
typingUsers
.contains(
dialog.opponentKey
),
isTyping,
isBlocked =
isBlocked,
isSavedMessages =
@@ -1181,7 +1169,6 @@ android.util.Log.d("ChatsListScreen", "✅ Total LaunchedEffect: ${System.curren
chatsViewModel.blockUser(
opponentKey
)
blocklistUpdateTrigger++
}
}
) { Text("Block", color = Color(0xFFFF3B30)) }
@@ -1222,7 +1209,6 @@ android.util.Log.d("ChatsListScreen", "✅ Total LaunchedEffect: ${System.curren
chatsViewModel.unblockUser(
opponentKey
)
blocklistUpdateTrigger++
}
}
) { Text("Unblock", color = PrimaryBlue) }
@@ -1921,13 +1907,19 @@ fun DialogItemContent(
}
} else {
// 🔥 Формируем displayName для инициалов в placeholder
val avatarDisplayName = when {
val avatarDisplayName = remember(
dialog.opponentTitle,
dialog.opponentKey,
dialog.opponentUsername
) {
when {
dialog.opponentTitle.isNotEmpty() &&
dialog.opponentTitle != dialog.opponentKey &&
!dialog.opponentTitle.startsWith(dialog.opponentKey.take(7)) -> dialog.opponentTitle
dialog.opponentUsername.isNotEmpty() -> dialog.opponentUsername
else -> null
}
}
com.rosetta.messenger.ui.components.AvatarImage(
publicKey = dialog.opponentKey,
avatarRepository = avatarRepository,
@@ -2090,11 +2082,11 @@ fun DialogItemContent(
}
}
val formattedTime = remember(dialog.lastMessageTimestamp) {
formatTime(Date(dialog.lastMessageTimestamp))
}
Text(
text =
formatTime(
Date(dialog.lastMessageTimestamp)
),
text = formattedTime,
fontSize = 13.sp,
color =
if (dialog.unreadCount > 0) PrimaryBlue
@@ -2147,12 +2139,13 @@ fun DialogItemContent(
// Unread badge
if (dialog.unreadCount > 0) {
Spacer(modifier = Modifier.width(8.dp))
val unreadText =
val unreadText = remember(dialog.unreadCount) {
when {
dialog.unreadCount > 999 -> "999+"
dialog.unreadCount > 99 -> "99+"
else -> dialog.unreadCount.toString()
}
}
Box(
modifier =
Modifier.height(22.dp)
@@ -2296,7 +2289,7 @@ fun RequestsScreen(
} else {
// Requests list
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(requests, key = { it.opponentKey }) { request ->
items(requests, key = { it.opponentKey }, contentType = { "request" }) { request ->
DialogItemContent(
dialog = request,
isDarkTheme = isDarkTheme,

View File

@@ -15,6 +15,7 @@ import com.rosetta.messenger.network.SearchUser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import androidx.compose.runtime.Immutable
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -22,6 +23,7 @@ import kotlinx.coroutines.withContext
/**
* UI модель диалога с расшифрованным lastMessage
*/
@Immutable
data class DialogUiModel(
val id: Long,
val account: String,
@@ -45,6 +47,7 @@ data class DialogUiModel(
* 🔥 Комбинированное состояние чатов для атомарного обновления UI
* Это предотвращает "дергание" когда dialogs и requests обновляются независимо
*/
@Immutable
data class ChatsUiState(
val dialogs: List<DialogUiModel> = emptyList(),
val requests: List<DialogUiModel> = emptyList(),
@@ -85,6 +88,10 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
private val _requestsCount = MutableStateFlow(0)
val requestsCount: StateFlow<Int> = _requestsCount.asStateFlow()
// Заблокированные пользователи (реактивный Set из Room Flow)
private val _blockedUsers = MutableStateFlow<Set<String>>(emptySet())
val blockedUsers: StateFlow<Set<String>> = _blockedUsers.asStateFlow()
// 🔥 НОВОЕ: Комбинированное состояние - обновляется атомарно!
// 🎯 ИСПРАВЛЕНИЕ: debounce предотвращает мерцание при быстрых обновлениях
val chatsState: StateFlow<ChatsUiState> = combine(
@@ -315,6 +322,17 @@ if (currentAccount == publicKey) {
_requestsCount.value = count
}
}
// 🔥 Подписываемся на блоклист — Room автоматически переэмитит при blockUser()/unblockUser()
viewModelScope.launch {
database.blacklistDao().getBlockedUsers(publicKey)
.flowOn(Dispatchers.IO)
.map { entities -> entities.map { it.publicKey }.toSet() }
.distinctUntilChanged()
.collect { blockedSet ->
_blockedUsers.value = blockedSet
}
}
}
/**

View File

@@ -32,6 +32,20 @@ import com.rosetta.messenger.utils.AvatarFileManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* LRU-кэш декодированных Bitmap аватаров (макс. 100 записей, ~11 MB)
*/
private object AvatarBitmapCache {
private val cache = object : LinkedHashMap<String, Bitmap>(50, 0.75f, true) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Bitmap>?): Boolean {
return size > 100
}
}
fun get(key: String): Bitmap? = synchronized(cache) { cache[key] }
fun put(key: String, bitmap: Bitmap) = synchronized(cache) { cache[key] = bitmap }
}
/**
* Composable для отображения аватара пользователя
* Совместимо с desktop версией (AvatarProvider)
@@ -80,15 +94,20 @@ fun AvatarImage(
LaunchedEffect(avatarKey, avatars.isEmpty()) {
val currentAvatars = avatars
if (currentAvatars.isNotEmpty()) {
val cacheKey = "${publicKey}_${avatarKey}"
val cachedBitmap = AvatarBitmapCache.get(cacheKey)
if (cachedBitmap != null) {
bitmap = cachedBitmap
} else {
val newBitmap = withContext(Dispatchers.IO) {
AvatarFileManager.base64ToBitmap(currentAvatars.first().base64Data)
}
// Устанавливаем новый bitmap только если декодирование успешно
if (newBitmap != null) {
AvatarBitmapCache.put(cacheKey, newBitmap)
bitmap = newBitmap
}
}
} else {
// 🔥 FIX: Если аватары удалены - сбрасываем bitmap чтобы показался placeholder
bitmap = null
}
}