optimize: optimize chatList
This commit is contained in:
@@ -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,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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user