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