From 58b754d5ba33313aa92e91e98ae606d0e5bca350 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sun, 8 Feb 2026 07:07:43 +0500 Subject: [PATCH] optimize: optimize chatList --- .../messenger/ui/chats/ChatsListScreen.kt | 71 +++++++++---------- .../messenger/ui/chats/ChatsListViewModel.kt | 18 +++++ .../messenger/ui/components/AvatarImage.kt | 33 +++++++-- 3 files changed, 76 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index 51dcef5..38150a0 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -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(null) } var dialogToUnblock by remember { mutableStateOf(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,12 +1907,18 @@ fun DialogItemContent( } } else { // 🔥 Формируем displayName для инициалов в placeholder - val avatarDisplayName = when { - dialog.opponentTitle.isNotEmpty() && - dialog.opponentTitle != dialog.opponentKey && - !dialog.opponentTitle.startsWith(dialog.opponentKey.take(7)) -> dialog.opponentTitle - dialog.opponentUsername.isNotEmpty() -> dialog.opponentUsername - else -> null + 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, @@ -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, diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt index 1e9ba44..6cbc194 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt @@ -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 = emptyList(), val requests: List = emptyList(), @@ -84,6 +87,10 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio // Количество requests private val _requestsCount = MutableStateFlow(0) val requestsCount: StateFlow = _requestsCount.asStateFlow() + + // Заблокированные пользователи (реактивный Set из Room Flow) + private val _blockedUsers = MutableStateFlow>(emptySet()) + val blockedUsers: StateFlow> = _blockedUsers.asStateFlow() // 🔥 НОВОЕ: Комбинированное состояние - обновляется атомарно! // 🎯 ИСПРАВЛЕНИЕ: debounce предотвращает мерцание при быстрых обновлениях @@ -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 + } + } } /** diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/AvatarImage.kt b/app/src/main/java/com/rosetta/messenger/ui/components/AvatarImage.kt index b3ee930..57b38fb 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/AvatarImage.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/AvatarImage.kt @@ -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(50, 0.75f, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): 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 newBitmap = withContext(Dispatchers.IO) { - AvatarFileManager.base64ToBitmap(currentAvatars.first().base64Data) - } - // Устанавливаем новый bitmap только если декодирование успешно - if (newBitmap != null) { - bitmap = newBitmap + 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) + } + if (newBitmap != null) { + AvatarBitmapCache.put(cacheKey, newBitmap) + bitmap = newBitmap + } } } else { - // 🔥 FIX: Если аватары удалены - сбрасываем bitmap чтобы показался placeholder bitmap = null } }