From 89e5f3cfa2c16bacdacb25170e3ade533366522e Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sun, 18 Jan 2026 13:12:43 +0500 Subject: [PATCH] feat: Optimize search animations and improve content rendering in SearchScreen; update Saved Messages icon handling in DialogItemContent --- .../com/rosetta/messenger/MainActivity.kt | 40 ++++----- .../messenger/network/ProtocolManager.kt | 24 +++++- .../ui/auth/ConfirmSeedPhraseScreen.kt | 6 +- .../ui/auth/ImportSeedPhraseScreen.kt | 6 +- .../messenger/ui/auth/SeedPhraseScreen.kt | 6 +- .../messenger/ui/auth/SelectAccountScreen.kt | 16 ++-- .../messenger/ui/auth/SetPasswordScreen.kt | 16 ++-- .../rosetta/messenger/ui/auth/UnlockScreen.kt | 2 +- .../messenger/ui/auth/WelcomeScreen.kt | 2 +- .../messenger/ui/chats/ChatDetailScreen.kt | 29 ++++--- .../messenger/ui/chats/ChatsListScreen.kt | 82 ++++++++++++------- .../messenger/ui/chats/ChatsListViewModel.kt | 24 +++++- .../ui/chats/ForwardChatPickerBottomSheet.kt | 6 +- .../messenger/ui/chats/SearchResultsList.kt | 55 +++++-------- .../messenger/ui/chats/SearchScreen.kt | 76 ++++++++++------- 15 files changed, 231 insertions(+), 159 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 3adf353..5992af7 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -171,8 +171,8 @@ class MainActivity : ComponentActivity() { else -> "main" }, transitionSpec = { - fadeIn(animationSpec = tween(600)) togetherWith - fadeOut(animationSpec = tween(600)) + fadeIn(animationSpec = tween(300)) togetherWith + fadeOut(animationSpec = tween(200)) }, label = "screenTransition" ) { screen -> @@ -375,20 +375,20 @@ fun MainScreen( val isExitingSearch = !targetState.second && initialState.second when { - // 🚀 Вход в чат - чистый slide справа (как Telegram/iOS) + // 🚀 Вход в чат - быстрый slide справа (как Telegram/iOS) // Новый экран полностью покрывает старый, никакой прозрачности isEnteringChat -> { slideInHorizontally( - initialOffsetX = { fullWidth -> fullWidth }, // Начинаем за экраном справа + initialOffsetX = { fullWidth -> fullWidth }, animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMediumLow // Плавно но быстро + stiffness = Spring.StiffnessHigh // 🔥 Быстрее для плавности ) ) togetherWith slideOutHorizontally( - targetOffsetX = { fullWidth -> -fullWidth / 4 }, // Старый экран уходит влево на 25% + targetOffsetX = { fullWidth -> -fullWidth / 5 }, // Меньше смещение animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMediumLow + stiffness = Spring.StiffnessHigh ) ) } @@ -396,33 +396,33 @@ fun MainScreen( // 🔙 Выход из чата - обратный slide isExitingChat -> { slideInHorizontally( - initialOffsetX = { fullWidth -> -fullWidth / 4 }, // Список возвращается слева + initialOffsetX = { fullWidth -> -fullWidth / 5 }, animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium // Чуть быстрее при выходе + stiffness = Spring.StiffnessHigh ) ) togetherWith slideOutHorizontally( - targetOffsetX = { fullWidth -> fullWidth }, // Чат уходит за экран вправо + targetOffsetX = { fullWidth -> fullWidth }, animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium + stiffness = Spring.StiffnessHigh ) ) } - // 🔍 Вход в поиск - slide справа (как чаты) + // 🔍 Вход в поиск - быстрый slide справа isEnteringSearch -> { slideInHorizontally( - initialOffsetX = { fullWidth -> fullWidth }, // Начинаем за экраном справа + initialOffsetX = { fullWidth -> fullWidth }, animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMediumLow + stiffness = Spring.StiffnessHigh // 🔥 Быстрее для плавности ) ) togetherWith slideOutHorizontally( - targetOffsetX = { fullWidth -> -fullWidth / 4 }, // Список уходит влево на 25% + targetOffsetX = { fullWidth -> -fullWidth / 5 }, // Меньше смещение animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMediumLow + stiffness = Spring.StiffnessHigh ) ) } @@ -430,16 +430,16 @@ fun MainScreen( // ❌ Выход из поиска - обратный slide isExitingSearch -> { slideInHorizontally( - initialOffsetX = { fullWidth -> -fullWidth / 4 }, // Список возвращается слева + initialOffsetX = { fullWidth -> -fullWidth / 5 }, animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium + stiffness = Spring.StiffnessHigh ) ) togetherWith slideOutHorizontally( - targetOffsetX = { fullWidth -> fullWidth }, // Поиск уходит за экран вправо + targetOffsetX = { fullWidth -> fullWidth }, animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium + stiffness = Spring.StiffnessHigh ) ) } diff --git a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt index da9c087..b00f4be 100644 --- a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt @@ -174,9 +174,27 @@ object ProtocolManager { } } - // 🔥 УБРАН обработчик поиска (0x03) из ProtocolManager - // Он вызывал бесконечный цикл т.к. updateDialogUserInfo триггерил Flow - // Обработка 0x03 происходит только в SearchUsersViewModel + // 🔥 Обработчик поиска/user info (0x03) + // Обновляет информацию о пользователе в диалогах когда приходит ответ от сервера + waitPacket(0x03) { packet -> + val searchPacket = packet as PacketSearch + addLog("🔍 Search/UserInfo response: ${searchPacket.users.size} users") + + // Обновляем информацию о пользователях в диалогах + if (searchPacket.users.isNotEmpty()) { + scope.launch { + searchPacket.users.forEach { user -> + addLog(" 📝 Updating user info: ${user.publicKey.take(16)}... title='${user.title}' username='${user.username}'") + messageRepository?.updateDialogUserInfo( + user.publicKey, + user.title, + user.username, + user.verified + ) + } + } + } + } } /** diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/ConfirmSeedPhraseScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/ConfirmSeedPhraseScreen.kt index cbd9cdf..353b2b9 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/ConfirmSeedPhraseScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/ConfirmSeedPhraseScreen.kt @@ -159,9 +159,9 @@ fun ConfirmSeedPhraseScreen( AnimatedVisibility( visible = visible, - enter = fadeIn(tween(500)) + slideInVertically( - initialOffsetY = { -20 }, - animationSpec = tween(500) + enter = fadeIn(tween(250)) + slideInVertically( + initialOffsetY = { -40 }, + animationSpec = tween(250) ) ) { Text( diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/ImportSeedPhraseScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/ImportSeedPhraseScreen.kt index c44b78c..7d21ce9 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/ImportSeedPhraseScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/ImportSeedPhraseScreen.kt @@ -78,9 +78,9 @@ fun ImportSeedPhraseScreen( AnimatedVisibility( visible = visible, - enter = fadeIn(tween(500)) + slideInVertically( - initialOffsetY = { -20 }, - animationSpec = tween(500) + enter = fadeIn(tween(250)) + slideInVertically( + initialOffsetY = { -40 }, + animationSpec = tween(250) ) ) { Text( diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/SeedPhraseScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/SeedPhraseScreen.kt index c05b0ee..569bc65 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/SeedPhraseScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/SeedPhraseScreen.kt @@ -80,9 +80,9 @@ fun SeedPhraseScreen( AnimatedVisibility( visible = visible, - enter = fadeIn(tween(500)) + slideInVertically( - initialOffsetY = { -20 }, - animationSpec = tween(500) + enter = fadeIn(tween(250)) + slideInVertically( + initialOffsetY = { -40 }, + animationSpec = tween(250) ) ) { Text( diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/SelectAccountScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/SelectAccountScreen.kt index 0a0cccd..7d0c0f8 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/SelectAccountScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/SelectAccountScreen.kt @@ -103,9 +103,9 @@ fun SelectAccountScreen( // Header AnimatedVisibility( visible = visible, - enter = fadeIn(tween(500)) + slideInVertically( - initialOffsetY = { -30 }, - animationSpec = tween(500) + enter = fadeIn(tween(250)) + slideInVertically( + initialOffsetY = { -40 }, + animationSpec = tween(250) ) ) { Column { @@ -269,9 +269,9 @@ private fun AccountListItem( AnimatedVisibility( visible = visible, - enter = fadeIn(tween(400)) + slideInHorizontally( - initialOffsetX = { -50 }, - animationSpec = tween(400) + enter = fadeIn(tween(200)) + slideInHorizontally( + initialOffsetX = { 50 }, + animationSpec = tween(200) ) ) { Card( @@ -378,9 +378,9 @@ private fun AddAccountButton( AnimatedVisibility( visible = visible, - enter = fadeIn(tween(400)) + slideInHorizontally( + enter = fadeIn(tween(200)) + slideInHorizontally( initialOffsetX = { -50 }, - animationSpec = tween(400) + animationSpec = tween(200) ) ) { Card( diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt index a878767..673609f 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt @@ -147,11 +147,11 @@ fun SetPasswordScreen( AnimatedVisibility( visible = visible, enter = - fadeIn(tween(500)) + + fadeIn(tween(250)) + scaleIn( initialScale = 0.5f, animationSpec = - tween(500, easing = FastOutSlowInEasing) + tween(250, easing = FastOutSlowInEasing) ) ) { Box( @@ -460,19 +460,19 @@ fun SetPasswordScreen( AnimatedVisibility( visible = visible && !isKeyboardVisible, enter = - fadeIn(tween(400)) + + fadeIn(tween(200)) + slideInVertically( initialOffsetY = { 30 }, - animationSpec = tween(400) + animationSpec = tween(200) ) + - scaleIn(initialScale = 0.9f, animationSpec = tween(400)), + scaleIn(initialScale = 0.9f, animationSpec = tween(200)), exit = - fadeOut(tween(300)) + + fadeOut(tween(150)) + slideOutVertically( targetOffsetY = { 30 }, - animationSpec = tween(300) + animationSpec = tween(150) ) + - scaleOut(targetScale = 0.9f, animationSpec = tween(300)) + scaleOut(targetScale = 0.9f, animationSpec = tween(150)) ) { Row( modifier = diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt index 15f1dbd..08c6810 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt @@ -181,7 +181,7 @@ fun UnlockScreen( // Rosetta Logo AnimatedVisibility( visible = visible, - enter = fadeIn(tween(600)) + scaleIn(tween(600, easing = FastOutSlowInEasing)) + enter = fadeIn(tween(300)) + scaleIn(tween(300, easing = FastOutSlowInEasing)) ) { Image( painter = painterResource(id = R.drawable.rosetta_icon), diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/WelcomeScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/WelcomeScreen.kt index d958f7d..bc6b54c 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/WelcomeScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/WelcomeScreen.kt @@ -97,7 +97,7 @@ fun WelcomeScreen( // Animated Lock Icon AnimatedVisibility( visible = visible, - enter = fadeIn(tween(600)) + scaleIn(tween(600, easing = FastOutSlowInEasing)) + enter = fadeIn(tween(300)) + scaleIn(tween(300, easing = FastOutSlowInEasing)) ) { Box(modifier = Modifier.size(180.dp), contentAlignment = Alignment.Center) { lockComposition?.let { comp -> diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 9f64e37..d828285 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -234,13 +234,22 @@ fun ChatDetailScreen( .getTotalUnreadCountExcludingFlow(currentUserPublicKey, user.publicKey) .collectAsState(initial = 0) - // Цвета как в React Native themes.ts - val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF) - val textColor = if (isDarkTheme) Color.White else Color.Black - val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) - val inputBackgroundColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF2F3F5) + // Цвета как в React Native themes.ts - 🔥 КЭШИРУЕМ для производительности + val backgroundColor = remember(isDarkTheme) { if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF) } + val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black } + val secondaryTextColor = remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) } + val inputBackgroundColor = remember(isDarkTheme) { if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF2F3F5) } // Цвет иконок в хедере - синий как в React Native - val headerIconColor = if (isDarkTheme) Color.White else PrimaryBlue + val headerIconColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else PrimaryBlue } + + // 🔥 ОПТИМИЗАЦИЯ: Отложенный рендеринг тяжелого контента + var isContentReady by remember { mutableStateOf(false) } + + // Запускаем отложенный рендеринг после завершения анимации перехода + LaunchedEffect(Unit) { + kotlinx.coroutines.delay(30) // Минимальная задержка для анимации + isContentReady = true + } // 🚀 Убираем дополнительную анимацию - используем только анимацию навигации из MainActivity @@ -380,8 +389,8 @@ fun ChatDetailScreen( // Проверяем, заблокирован ли пользователь (отложенно, не блокирует UI) var isBlocked by remember { mutableStateOf(false) } LaunchedEffect(user.publicKey, currentUserPublicKey) { - // Отложенная проверка - не блокирует анимацию - kotlinx.coroutines.delay(50) // Даём анимации завершиться + // 🔥 ОПТИМИЗАЦИЯ: Отложенная проверка - не блокирует анимацию + kotlinx.coroutines.delay(100) // Даём анимации завершиться isBlocked = database.blacklistDao().isUserBlocked(user.publicKey, currentUserPublicKey) } @@ -572,10 +581,10 @@ fun ChatDetailScreen( } else headerBackground ) ) { - // Контент хедера с Crossfade для плавной смены + // Контент хедера с Crossfade для плавной смены - ускоренная анимация Crossfade( targetState = isSelectionMode, - animationSpec = tween(200), + animationSpec = tween(150), label = "headerContent" ) { selectionMode -> if (selectionMode) { 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 419e15a..142ab76 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 @@ -172,11 +172,11 @@ fun ChatsListScreen( } } - // Colors - instant change, no animation - val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF) - val drawerBackgroundColor = if (isDarkTheme) Color(0xFF212121) else Color(0xFFFFFFFF) - val textColor = if (isDarkTheme) Color.White else Color.Black - val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E8E) else Color(0xFF8E8E93) + // Colors - instant change, no animation - 🔥 КЭШИРУЕМ для производительности + val backgroundColor = remember(isDarkTheme) { if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF) } + val drawerBackgroundColor = remember(isDarkTheme) { if (isDarkTheme) Color(0xFF212121) else Color(0xFFFFFFFF) } + val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black } + val secondaryTextColor = remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E8E) else Color(0xFF8E8E93) } // Protocol connection state val protocolState by ProtocolManager.state.collectAsState() @@ -479,12 +479,12 @@ fun ChatsListScreen( val context = androidx.compose.ui.platform.LocalContext.current var showCopiedToast by remember { mutableStateOf(false) } - // Плавная замена текста + // Плавная замена текста - ускоренная анимация AnimatedContent( targetState = showCopiedToast, transitionSpec = { - fadeIn(animationSpec = tween(300)) togetherWith - fadeOut(animationSpec = tween(300)) + fadeIn(animationSpec = tween(150)) togetherWith + fadeOut(animationSpec = tween(150)) }, label = "copiedAnimation" ) { isCopied -> @@ -894,35 +894,35 @@ fun ChatsListScreen( targetState = showRequestsScreen, transitionSpec = { if (targetState) { - // Переход на Requests: slide in from right + fade + // Переход на Requests: быстрый slide + fade (slideInHorizontally( animationSpec = - tween(300, easing = FastOutSlowInEasing), + tween(200, easing = FastOutSlowInEasing), initialOffsetX = { it } - ) + fadeIn(tween(300))) togetherWith + ) + fadeIn(tween(150))) togetherWith (slideOutHorizontally( animationSpec = tween( - 300, + 200, easing = FastOutSlowInEasing ), - targetOffsetX = { -it / 3 } - ) + fadeOut(tween(200))) + targetOffsetX = { -it / 4 } + ) + fadeOut(tween(100))) } else { // Возврат из Requests: slide out to right (slideInHorizontally( animationSpec = - tween(300, easing = FastOutSlowInEasing), - initialOffsetX = { -it / 3 } - ) + fadeIn(tween(300))) togetherWith + tween(200, easing = FastOutSlowInEasing), + initialOffsetX = { -it / 4 } + ) + fadeIn(tween(150))) togetherWith (slideOutHorizontally( animationSpec = tween( - 300, + 200, easing = FastOutSlowInEasing ), targetOffsetX = { it } - ) + fadeOut(tween(200))) + ) + fadeOut(tween(100))) } }, label = "RequestsTransition" @@ -1554,25 +1554,35 @@ fun DialogItemContent( // 📁 Для Saved Messages показываем специальное имя val displayName = - remember(dialog.opponentTitle, dialog.opponentKey, dialog.isSavedMessages) { + remember(dialog.opponentTitle, dialog.opponentUsername, dialog.opponentKey, dialog.isSavedMessages) { if (dialog.isSavedMessages) { "Saved Messages" + } else if (dialog.opponentTitle.isNotEmpty()) { + // 🔥 Показываем title как основное имя (как в десктопной версии) + dialog.opponentTitle + } else if (dialog.opponentUsername.isNotEmpty()) { + // Username только если нет title + "@${dialog.opponentUsername}" } else { - dialog.opponentTitle.ifEmpty { dialog.opponentKey.take(8) } + dialog.opponentKey.take(8) } } // 📁 Для Saved Messages показываем иконку закладки val initials = - remember(dialog.opponentTitle, dialog.opponentKey, dialog.isSavedMessages) { + remember(dialog.opponentTitle, dialog.opponentUsername, dialog.opponentKey, dialog.isSavedMessages) { if (dialog.isSavedMessages) { - "📁" // Иконка для Saved Messages + "" // Для Saved Messages - пустая строка, будет использоваться иконка } else if (dialog.opponentTitle.isNotEmpty()) { + // Используем title для инициалов dialog.opponentTitle .split(" ") .take(2) .mapNotNull { it.firstOrNull()?.uppercase() } .joinToString("") + } else if (dialog.opponentUsername.isNotEmpty()) { + // Если только username - берем первые 2 символа + dialog.opponentUsername.take(2).uppercase() } else { dialog.opponentKey.take(2).uppercase() } @@ -1592,15 +1602,27 @@ fun DialogItemContent( modifier = Modifier.fillMaxSize() .clip(CircleShape) - .background(avatarColors.backgroundColor), + .background( + if (dialog.isSavedMessages) PrimaryBlue + else avatarColors.backgroundColor + ), contentAlignment = Alignment.Center ) { - Text( - text = initials, - color = avatarColors.textColor, - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp - ) + if (dialog.isSavedMessages) { + Icon( + Icons.Default.Bookmark, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(24.dp) + ) + } else { + Text( + text = initials, + color = avatarColors.textColor, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp + ) + } } // Online indicator - зелёный кружок с белой обводкой 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 48591bf..7ccafa0 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 @@ -107,6 +107,14 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio .map { dialogsList -> // 🔓 Расшифровываем lastMessage на IO потоке (PBKDF2 - тяжелая операция!) dialogsList.map { dialog -> + // 🔥 Загружаем информацию о пользователе если её нет + // 📁 НЕ загружаем для Saved Messages + val isSavedMessages = (dialog.account == dialog.opponentKey) + if (!isSavedMessages && (dialog.opponentTitle.isEmpty() || dialog.opponentTitle == dialog.opponentKey || dialog.opponentTitle == dialog.opponentKey.take(7))) { + android.util.Log.d("ChatsListVM", "🔍 Dialog needs user info: ${dialog.opponentKey.take(16)}... title='${dialog.opponentTitle}'") + loadUserInfoForDialog(dialog.opponentKey) + } + val decryptedLastMessage = try { if (privateKey.isNotEmpty() && dialog.lastMessage.isNotEmpty()) { CryptoManager.decryptWithPassword(dialog.lastMessage, privateKey) @@ -130,7 +138,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio isOnline = dialog.isOnline, lastSeen = dialog.lastSeen, verified = dialog.verified, - isSavedMessages = (dialog.account == dialog.opponentKey) // 📁 Saved Messages + isSavedMessages = isSavedMessages // 📁 Saved Messages ) } } @@ -372,20 +380,28 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio * 📁 НЕ загружаем для Saved Messages (свой publicKey) */ private fun loadUserInfoForRequest(publicKey: String) { + loadUserInfoForDialog(publicKey) + } + + /** + * 🔥 Загрузить информацию о пользователе для диалога + * 📁 НЕ загружаем для Saved Messages (свой publicKey) + */ + private fun loadUserInfoForDialog(publicKey: String) { // 📁 Не запрашиваем информацию о самом себе (Saved Messages) if (publicKey == currentAccount) { - android.util.Log.d("ChatsListVM", "📁 Skipping loadUserInfoForRequest for Saved Messages") + android.util.Log.d("ChatsListVM", "📁 Skipping loadUserInfoForDialog for Saved Messages") return } // 🔥 Не запрашиваем если уже запрашивали if (requestedUserInfoKeys.contains(publicKey)) { - android.util.Log.d("ChatsListVM", "⏭️ Skipping loadUserInfoForRequest - already requested for ${publicKey.take(16)}...") + android.util.Log.d("ChatsListVM", "⏭️ Skipping loadUserInfoForDialog - already requested for ${publicKey.take(16)}...") return } requestedUserInfoKeys.add(publicKey) - android.util.Log.d("ChatsListVM", "🔍 loadUserInfoForRequest: ${publicKey.take(16)}...") + android.util.Log.d("ChatsListVM", "🔍 loadUserInfoForDialog: ${publicKey.take(16)}...") viewModelScope.launch(Dispatchers.IO) { try { diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ForwardChatPickerBottomSheet.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ForwardChatPickerBottomSheet.kt index 0b337a1..16a99fd 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ForwardChatPickerBottomSheet.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ForwardChatPickerBottomSheet.kt @@ -208,16 +208,17 @@ private fun ForwardDialogItem( } val displayName = - remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) { + remember(dialog.opponentTitle, dialog.opponentUsername, dialog.opponentKey, isSavedMessages) { when { isSavedMessages -> "Saved Messages" dialog.opponentTitle.isNotEmpty() -> dialog.opponentTitle + dialog.opponentUsername.isNotEmpty() -> "@${dialog.opponentUsername}" else -> dialog.opponentKey.take(8) } } val initials = - remember(dialog.opponentTitle, dialog.opponentKey, isSavedMessages) { + remember(dialog.opponentTitle, dialog.opponentUsername, dialog.opponentKey, isSavedMessages) { when { isSavedMessages -> "📁" dialog.opponentTitle.isNotEmpty() -> { @@ -227,6 +228,7 @@ private fun ForwardDialogItem( .mapNotNull { it.firstOrNull()?.uppercase() } .joinToString("") } + dialog.opponentUsername.isNotEmpty() -> dialog.opponentUsername.take(2).uppercase() else -> dialog.opponentKey.take(2).uppercase() } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/SearchResultsList.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/SearchResultsList.kt index 26a9bb1..452cc76 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/SearchResultsList.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/SearchResultsList.kt @@ -16,21 +16,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.airbnb.lottie.compose.* import com.airbnb.lottie.LottieComposition -import com.rosetta.messenger.R import com.rosetta.messenger.network.SearchUser import com.rosetta.messenger.ui.components.VerifiedBadge import com.rosetta.messenger.ui.onboarding.PrimaryBlue /** * Компонент отображения результатов поиска пользователей - * Минималистичный дизайн с плавными анимациями + * Оптимизированный дизайн с минимальными анимациями для плавной работы */ @Composable fun SearchResultsList( @@ -46,11 +44,11 @@ fun SearchResultsList( val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) Box(modifier = modifier.fillMaxSize()) { - // Loading state + // Loading state - упрощенная анимация AnimatedVisibility( visible = isSearching, - enter = fadeIn(animationSpec = tween(200)), - exit = fadeOut(animationSpec = tween(150)) + enter = fadeIn(animationSpec = snap()), + exit = fadeOut(animationSpec = tween(100)) ) { Box( modifier = Modifier @@ -79,8 +77,8 @@ fun SearchResultsList( // Empty state - поднимается при открытии клавиатуры AnimatedVisibility( visible = !isSearching && searchResults.isEmpty(), - enter = fadeIn(animationSpec = tween(300)), - exit = fadeOut(animationSpec = tween(200)) + enter = fadeIn(animationSpec = tween(150)), + exit = fadeOut(animationSpec = tween(100)) ) { Box( modifier = Modifier @@ -125,40 +123,27 @@ fun SearchResultsList( } } - // Results list with staggered animation + // Results list - упрощенная анимация без staggered AnimatedVisibility( visible = !isSearching && searchResults.isNotEmpty(), - enter = fadeIn(animationSpec = tween(250)), - exit = fadeOut(animationSpec = tween(150)) + enter = fadeIn(animationSpec = tween(150)), + exit = fadeOut(animationSpec = tween(100)) ) { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(vertical = 4.dp) ) { - itemsIndexed(searchResults) { index, user -> - // Staggered animation - var isVisible by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - kotlinx.coroutines.delay(index * 40L) - isVisible = true - } - - AnimatedVisibility( - visible = isVisible, - enter = fadeIn(tween(200)) + slideInHorizontally( - initialOffsetX = { it / 3 }, - animationSpec = tween(250, easing = FastOutSlowInEasing) - ), - exit = fadeOut(tween(100)) - ) { - SearchResultItem( - user = user, - isOwnAccount = user.publicKey == currentUserPublicKey, - isDarkTheme = isDarkTheme, - isLastItem = index == searchResults.size - 1, - onClick = { onUserClick(user) } - ) - } + itemsIndexed( + items = searchResults, + key = { _, user -> user.publicKey } + ) { index, user -> + SearchResultItem( + user = user, + isOwnAccount = user.publicKey == currentUserPublicKey, + isDarkTheme = isDarkTheme, + isLastItem = index == searchResults.size - 1, + onClick = { onUserClick(user) } + ) } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt index 6c7cbd3..ac6046c 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/SearchScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color @@ -27,6 +28,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel import com.airbnb.lottie.compose.* import com.rosetta.messenger.R import com.rosetta.messenger.data.RecentSearchesManager @@ -59,46 +61,54 @@ fun SearchScreen( focusManager.clearFocus() } - // Цвета ТОЧНО как в ChatsListScreen - val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF) - val textColor = if (isDarkTheme) Color.White else Color(0xFF1a1a1a) - val secondaryTextColor = if (isDarkTheme) Color(0xFFB0B0B0) else Color(0xFF6c757d) - val surfaceColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color.White + // Цвета ТОЧНО как в ChatsListScreen - remember для избежания пересоздания + val backgroundColor = remember(isDarkTheme) { if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF) } + val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color(0xFF1a1a1a) } + val secondaryTextColor = remember(isDarkTheme) { if (isDarkTheme) Color(0xFFB0B0B0) else Color(0xFF6c757d) } - // Search ViewModel - val searchViewModel = remember { SearchUsersViewModel() } + // 🔥 ОПТИМИЗАЦИЯ: Отложенный рендеринг контента для плавной анимации перехода + var isContentReady by remember { mutableStateOf(false) } + + // Search ViewModel - правильное создание через viewModel() + val searchViewModel: SearchUsersViewModel = viewModel() val searchQuery by searchViewModel.searchQuery.collectAsState() val searchResults by searchViewModel.searchResults.collectAsState() val isSearching by searchViewModel.isSearching.collectAsState() - // Recent users (не текстовые запросы, а пользователи) + // Recent users - отложенная подписка val recentUsers by RecentSearchesManager.recentUsers.collectAsState() - // Preload Lottie composition for search animation - val searchLottieComposition by - rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.search)) - - // Устанавливаем аккаунт для RecentSearchesManager - LaunchedEffect(currentUserPublicKey) { - if (currentUserPublicKey.isNotEmpty()) { - RecentSearchesManager.setAccount(currentUserPublicKey) - } - } - - // Устанавливаем privateKeyHash - LaunchedEffect(privateKeyHash) { - if (privateKeyHash.isNotEmpty()) { - searchViewModel.setPrivateKeyHash(privateKeyHash) - } - } + // 🔥 ОПТИМИЗАЦИЯ: Lottie загружается асинхронно и не блокирует первый кадр + val searchLottieComposition by rememberLottieComposition( + LottieCompositionSpec.RawRes(R.raw.search) + ) // Focus requester для автофокуса val focusRequester = remember { FocusRequester() } - // Автофокус при открытии + // 🔥 ОПТИМИЗАЦИЯ: Все тяжелые операции выполняем после первого кадра LaunchedEffect(Unit) { + // Даем анимации перехода завершиться + kotlinx.coroutines.delay(50) + isContentReady = true + + // Устанавливаем аккаунт для RecentSearchesManager + if (currentUserPublicKey.isNotEmpty()) { + RecentSearchesManager.setAccount(currentUserPublicKey) + } + + // Устанавливаем privateKeyHash + if (privateKeyHash.isNotEmpty()) { + searchViewModel.setPrivateKeyHash(privateKeyHash) + } + + // Автофокус с небольшой задержкой kotlinx.coroutines.delay(100) - focusRequester.requestFocus() + try { + focusRequester.requestFocus() + } catch (e: Exception) { + // Игнорируем ошибки фокуса + } } Scaffold( @@ -193,7 +203,17 @@ fun SearchScreen( containerColor = backgroundColor ) { paddingValues -> // Контент - показываем recent users если поле пустое, иначе результаты - Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + // 🔥 ОПТИМИЗАЦИЯ: Скрываем контент до готовности без блокировки рендера + .drawWithContent { + if (isContentReady) { + drawContent() + } + } + ) { if (searchQuery.isEmpty() && recentUsers.isNotEmpty()) { // Recent Users с аватарками LazyColumn(