feat: Optimize search animations and improve content rendering in SearchScreen; update Saved Messages icon handling in DialogItemContent
This commit is contained in:
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 - зелёный кружок с белой обводкой
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user