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

View File

@@ -15,6 +15,7 @@ import com.rosetta.messenger.network.SearchUser
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import androidx.compose.runtime.Immutable
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -22,6 +23,7 @@ import kotlinx.coroutines.withContext
/** /**
* UI модель диалога с расшифрованным lastMessage * UI модель диалога с расшифрованным lastMessage
*/ */
@Immutable
data class DialogUiModel( data class DialogUiModel(
val id: Long, val id: Long,
val account: String, val account: String,
@@ -45,6 +47,7 @@ data class DialogUiModel(
* 🔥 Комбинированное состояние чатов для атомарного обновления UI * 🔥 Комбинированное состояние чатов для атомарного обновления UI
* Это предотвращает "дергание" когда dialogs и requests обновляются независимо * Это предотвращает "дергание" когда dialogs и requests обновляются независимо
*/ */
@Immutable
data class ChatsUiState( data class ChatsUiState(
val dialogs: List<DialogUiModel> = emptyList(), val dialogs: List<DialogUiModel> = emptyList(),
val requests: List<DialogUiModel> = emptyList(), val requests: List<DialogUiModel> = emptyList(),
@@ -84,6 +87,10 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
// Количество requests // Количество requests
private val _requestsCount = MutableStateFlow(0) private val _requestsCount = MutableStateFlow(0)
val requestsCount: StateFlow<Int> = _requestsCount.asStateFlow() val requestsCount: StateFlow<Int> = _requestsCount.asStateFlow()
// Заблокированные пользователи (реактивный Set из Room Flow)
private val _blockedUsers = MutableStateFlow<Set<String>>(emptySet())
val blockedUsers: StateFlow<Set<String>> = _blockedUsers.asStateFlow()
// 🔥 НОВОЕ: Комбинированное состояние - обновляется атомарно! // 🔥 НОВОЕ: Комбинированное состояние - обновляется атомарно!
// 🎯 ИСПРАВЛЕНИЕ: debounce предотвращает мерцание при быстрых обновлениях // 🎯 ИСПРАВЛЕНИЕ: debounce предотвращает мерцание при быстрых обновлениях
@@ -315,6 +322,17 @@ if (currentAccount == publicKey) {
_requestsCount.value = count _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.Dispatchers
import kotlinx.coroutines.withContext 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 для отображения аватара пользователя * Composable для отображения аватара пользователя
* Совместимо с desktop версией (AvatarProvider) * Совместимо с desktop версией (AvatarProvider)
@@ -80,15 +94,20 @@ fun AvatarImage(
LaunchedEffect(avatarKey, avatars.isEmpty()) { LaunchedEffect(avatarKey, avatars.isEmpty()) {
val currentAvatars = avatars val currentAvatars = avatars
if (currentAvatars.isNotEmpty()) { if (currentAvatars.isNotEmpty()) {
val newBitmap = withContext(Dispatchers.IO) { val cacheKey = "${publicKey}_${avatarKey}"
AvatarFileManager.base64ToBitmap(currentAvatars.first().base64Data) val cachedBitmap = AvatarBitmapCache.get(cacheKey)
} if (cachedBitmap != null) {
// Устанавливаем новый bitmap только если декодирование успешно bitmap = cachedBitmap
if (newBitmap != null) { } else {
bitmap = newBitmap val newBitmap = withContext(Dispatchers.IO) {
AvatarFileManager.base64ToBitmap(currentAvatars.first().base64Data)
}
if (newBitmap != null) {
AvatarBitmapCache.put(cacheKey, newBitmap)
bitmap = newBitmap
}
} }
} else { } else {
// 🔥 FIX: Если аватары удалены - сбрасываем bitmap чтобы показался placeholder
bitmap = null bitmap = null
} }
} }