diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 275f43d..8f435d9 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -285,16 +285,16 @@ fun MainScreen( ) } - // 🔍 Вход в поиск - slide сверху + // 🔍 Вход в поиск - slide справа (как чаты) isEnteringSearch -> { - slideInVertically( - initialOffsetY = { -it }, + slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, // Начинаем за экраном справа animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow ) - ) togetherWith slideOutVertically( - targetOffsetY = { it / 4 }, + ) togetherWith slideOutHorizontally( + targetOffsetX = { fullWidth -> -fullWidth / 4 }, // Список уходит влево на 25% animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow @@ -302,16 +302,16 @@ fun MainScreen( ) } - // ❌ Выход из поиска + // ❌ Выход из поиска - обратный slide isExitingSearch -> { - slideInVertically( - initialOffsetY = { it / 4 }, + slideInHorizontally( + initialOffsetX = { fullWidth -> -fullWidth / 4 }, // Список возвращается слева animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMedium ) - ) togetherWith slideOutVertically( - targetOffsetY = { -it }, + ) togetherWith slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, // Поиск уходит за экран вправо animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMedium 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 ca2cecc..b6e5af2 100644 --- a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt @@ -103,10 +103,17 @@ object ProtocolManager { addLog("🟢 Online status received: ${onlinePacket.publicKeysState.size} entries") scope.launch { - onlinePacket.publicKeysState.forEach { item -> - val isOnline = item.state == OnlineState.ONLINE - addLog(" ${item.publicKey.take(16)}... -> ${if (isOnline) "ONLINE" else "OFFLINE"}") - messageRepository?.updateOnlineStatus(item.publicKey, isOnline) + if (messageRepository == null) { + addLog("❌ ERROR: messageRepository is NULL!") + } else { + addLog("✅ messageRepository is initialized") + onlinePacket.publicKeysState.forEach { item -> + val isOnline = item.state == OnlineState.ONLINE + addLog(" ${item.publicKey.take(16)}... -> ${if (isOnline) "ONLINE" else "OFFLINE"}") + addLog(" Calling updateOnlineStatus...") + messageRepository?.updateOnlineStatus(item.publicKey, isOnline) + addLog(" updateOnlineStatus called") + } } } } 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 7adebc7..293cabb 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 @@ -639,30 +639,35 @@ fun ChatDetailScreen( keyboardController?.hide() focusManager.clearFocus() showMenu = true - } + }, + modifier = Modifier + .size(48.dp) + .clip(CircleShape) ) { Icon( Icons.Default.MoreVert, contentDescription = "More", - tint = headerIconColor + tint = headerIconColor, + modifier = Modifier.size(26.dp) ) } // Выпадающее меню - чистый дизайн без артефактов - DropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false }, - modifier = Modifier - .width(220.dp) - .clip(RoundedCornerShape(16.dp)) - .background( - color = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White - ) - .shadow( - elevation = 8.dp, - shape = RoundedCornerShape(16.dp) - ) + MaterialTheme( + colorScheme = MaterialTheme.colorScheme.copy( + surface = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White + ) ) { + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + modifier = Modifier + .width(220.dp) + .background( + color = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White, + shape = RoundedCornerShape(16.dp) + ) + ) { // Delete Chat - красный DropdownMenuItem( text = { @@ -689,7 +694,10 @@ fun ChatDetailScreen( showMenu = false showDeleteConfirm = true }, - modifier = Modifier.padding(horizontal = 8.dp) + modifier = Modifier.padding(horizontal = 8.dp), + colors = MenuDefaults.itemColors( + textColor = Color(0xFFE53935) + ) ) // Разделитель @@ -733,7 +741,10 @@ fun ChatDetailScreen( showBlockConfirm = true } }, - modifier = Modifier.padding(horizontal = 8.dp) + modifier = Modifier.padding(horizontal = 8.dp), + colors = MenuDefaults.itemColors( + textColor = PrimaryBlue + ) ) // Разделитель @@ -771,8 +782,12 @@ fun ChatDetailScreen( showMenu = false showLogs = true }, - modifier = Modifier.padding(horizontal = 8.dp) + modifier = Modifier.padding(horizontal = 8.dp), + colors = MenuDefaults.itemColors( + textColor = textColor + ) ) + } } } } @@ -797,10 +812,16 @@ fun ChatDetailScreen( Box( modifier = Modifier .fillMaxSize() + .graphicsLayer { clip = false } .padding(paddingValues) ) { // Список сообщений - динамический padding для клавиатуры/эмодзи - Box(modifier = Modifier.fillMaxSize().padding(bottom = listBottomPadding)) { + // 🔥 graphicsLayer(clip = false) - позволяет пузырькам выходить за границы padding + Box(modifier = Modifier + .fillMaxSize() + .graphicsLayer { clip = false } + .padding(bottom = listBottomPadding) + ) { when { // 🔥 СКЕЛЕТОН - показываем пока загружаются сообщения isLoading -> { @@ -1483,11 +1504,12 @@ private fun MessageBubble( label = "selectionAlpha" ) - // 🔥 ОПТИМИЗАЦИЯ: Кешируем цвета - они не меняются для одного сообщения + // 🔥 Цвета - НАШИ ОРИГИНАЛЬНЫЕ val bubbleColor = remember(message.isOutgoing, isDarkTheme) { if (message.isOutgoing) { - PrimaryBlue + PrimaryBlue // Исходящие - наш синий } else { + // Входящие - наши цвета if (isDarkTheme) Color(0xFF212121) else Color(0xFFF5F5F5) } } @@ -1495,18 +1517,20 @@ private fun MessageBubble( if (message.isOutgoing) Color.White else if (isDarkTheme) Color.White else Color(0xFF000000) } + // Время - наши оригинальные цвета val timeColor = remember(message.isOutgoing, isDarkTheme) { if (message.isOutgoing) Color.White.copy(alpha = 0.7f) else if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) } - // 🔥 ОПТИМИЗАЦИЯ: Кешируем форму bubble + // 🔥 TELEGRAM STYLE: Форма пузырька - более мягкие углы val bubbleShape = remember(message.isOutgoing, showTail) { RoundedCornerShape( - topStart = 18.dp, - topEnd = 18.dp, - bottomStart = if (message.isOutgoing) 18.dp else (if (showTail) 4.dp else 18.dp), - bottomEnd = if (message.isOutgoing) (if (showTail) 4.dp else 18.dp) else 18.dp + topStart = 16.dp, + topEnd = 16.dp, + // Хвостик: маленький радиус (4dp) только у нижнего угла со стороны отправителя + bottomStart = if (message.isOutgoing) 16.dp else (if (showTail) 4.dp else 16.dp), + bottomEnd = if (message.isOutgoing) (if (showTail) 4.dp else 16.dp) else 16.dp ) } @@ -1574,11 +1598,10 @@ private fun MessageBubble( Row( modifier = Modifier.fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 1.dp) + // 🔥 TELEGRAM: horizontal 4dp (меньше), vertical 2dp (компактнее) + .padding(horizontal = 4.dp, vertical = 2.dp) .offset { IntOffset(animatedOffset.toInt(), 0) } .graphicsLayer { - // ❌ УБРАЛИ: alpha = alpha * selectionAlpha и translationY - // Оставляем только selection анимацию this.alpha = selectionAlpha this.scaleX = selectionScale this.scaleY = selectionScale @@ -1607,7 +1630,7 @@ private fun MessageBubble( Box( modifier = - Modifier.widthIn(max = 300.dp) + Modifier.widthIn(max = 280.dp) // 🔥 TELEGRAM: чуть уже (280dp) .combinedClickable( indication = null, interactionSource = remember { MutableInteractionSource() }, @@ -1616,9 +1639,10 @@ private fun MessageBubble( ) .clip(bubbleShape) .background(bubbleColor) - .padding(horizontal = 12.dp, vertical = 7.dp) + // 🔥 TELEGRAM: padding 10-12dp horizontal, 8dp vertical + .padding(horizontal = 10.dp, vertical = 8.dp) ) { - // 🔥 Telegram-style: текст и время на одной строке, выровнены по нижней границе + // 🔥 TELEGRAM STYLE: текст и время на одной строке Column { // Reply bubble (цитата) message.replyData?.let { reply -> @@ -1627,32 +1651,34 @@ private fun MessageBubble( isOutgoing = message.isOutgoing, isDarkTheme = isDarkTheme ) - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(4.dp)) // Меньше отступ } // Текст и время в одной строке (Row) Row( - verticalAlignment = Alignment.Bottom, // Выравнивание по нижней границе (baseline) - horizontalArrangement = Arrangement.spacedBy(6.dp) + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.spacedBy(8.dp) // Чуть больше отступ до времени ) { - // Текст (не растягивается на всю ширину) + // 🔥 TELEGRAM: Текст 17sp, lineHeight 22sp, letterSpacing -0.4sp AppleEmojiText( text = message.text, color = textColor, - fontSize = 16.sp, + fontSize = 17.sp, modifier = Modifier.weight(1f, fill = false) ) // Время и статус справа Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(3.dp), - modifier = Modifier.padding(bottom = 1.dp) // Небольшая коррекция для выравнивания + horizontalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.padding(bottom = 2.dp) ) { + // 🔥 TELEGRAM: Время 11sp, italic style Text( text = timeFormat.format(message.timestamp), color = timeColor, - fontSize = 11.sp + fontSize = 11.sp, + fontStyle = androidx.compose.ui.text.font.FontStyle.Italic ) if (message.isOutgoing) { AnimatedMessageStatus( @@ -1781,7 +1807,7 @@ private fun AnimatedMessageStatus( } /** - * 🔥 Reply bubble (цитата) внутри сообщения - как в React Native + * 🔥 Reply bubble (цитата) внутри сообщения * Стиль: вертикальная линия слева + имя + текст */ @Composable @@ -1790,7 +1816,7 @@ private fun ReplyBubble( isOutgoing: Boolean, isDarkTheme: Boolean ) { - // Цвета как в React Native + // НАШИ ОРИГИНАЛЬНЫЕ ЦВЕТА val backgroundColor = if (isOutgoing) { Color.Black.copy(alpha = 0.15f) } else { @@ -1809,7 +1835,7 @@ private fun ReplyBubble( PrimaryBlue } - val textColor = if (isOutgoing) { + val replyTextColor = if (isOutgoing) { Color.White.copy(alpha = 0.85f) } else { if (isDarkTheme) Color.White else Color.Black @@ -1817,12 +1843,12 @@ private fun ReplyBubble( Row( modifier = Modifier - .fillMaxWidth() + .wrapContentWidth() .height(IntrinsicSize.Min) .clip(RoundedCornerShape(4.dp)) .background(backgroundColor) ) { - // Вертикальная линия слева (как borderLeft в React Native) + // 🔥 TELEGRAM: Вертикальная линия слева 3dp Box( modifier = Modifier .width(3.dp) @@ -1833,22 +1859,24 @@ private fun ReplyBubble( // Контент Column( modifier = Modifier - .padding(start = 10.dp, end = 10.dp, top = 6.dp, bottom = 6.dp) + // 🔥 TELEGRAM: padding как в дизайне + .padding(start = 8.dp, end = 10.dp, top = 4.dp, bottom = 4.dp) + .widthIn(max = 220.dp) ) { - // Имя отправителя цитируемого сообщения + // 🔥 TELEGRAM: Имя 14sp, Medium weight Text( text = replyData.senderName, color = nameColor, fontSize = 14.sp, - fontWeight = FontWeight.SemiBold, + fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis ) - // Текст цитируемого сообщения + // 🔥 TELEGRAM: Текст цитаты 14sp, Regular Text( text = replyData.text.ifEmpty { "..." }, - color = textColor, + color = replyTextColor, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis @@ -2007,6 +2035,7 @@ private fun MessageInputBar( Column( modifier = Modifier .fillMaxWidth() + .graphicsLayer { clip = false } .windowInsetsPadding(WindowInsets.ime.only(WindowInsetsSides.Bottom)) ) { // Если пользователь заблокирован - показываем BlockedChatFooter (плоский как инпут) @@ -2053,6 +2082,7 @@ private fun MessageInputBar( Column( modifier = Modifier .fillMaxWidth() + .graphicsLayer { clip = false } ) { // Верхний border (как в архиве) Box( @@ -2240,8 +2270,8 @@ private fun MessageInputBar( // Мгновенно когда клавиатура открывается snap() } else { - // Быстрая анимация для мгновенного отклика (как в Telegram) - tween(durationMillis = 150, easing = TelegramEasing) + // Быстрая анимация как обычная клавиатура (200ms) + tween(durationMillis = 200, easing = FastOutSlowInEasing) }, label = "EmojiPanelHeight" ) @@ -2250,10 +2280,9 @@ private fun MessageInputBar( modifier = Modifier .fillMaxWidth() .height(animatedHeight) - .clipToBounds() ) { // 🚀 Рендерим панель только когда нужно - if (showEmojiPicker && !isKeyboardVisible && animatedHeight > 0.dp) { + if (showEmojiPicker && !isKeyboardVisible) { AppleEmojiPickerPanel( isDarkTheme = isDarkTheme, onEmojiSelected = { emoji -> diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiPicker.kt b/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiPicker.kt index 73ad67d..c021d08 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiPicker.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiPicker.kt @@ -36,6 +36,7 @@ import coil.request.ImageRequest import com.rosetta.messenger.ui.onboarding.PrimaryBlue import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext /** @@ -505,14 +506,8 @@ fun EmojiButton( val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() - val scale by animateFloatAsState( - targetValue = if (isPressed) 0.85f else 1f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessHigh - ), - label = "emojiScale" - ) + // 🚀 Убираем анимацию scale для производительности + val scale = if (isPressed) 0.85f else 1f val imageRequest = remember(unified) { ImageRequest.Builder(context) @@ -613,11 +608,15 @@ fun AppleEmojiPickerPanel( var selectedCategory by remember { mutableStateOf(EMOJI_CATEGORIES[0]) } val gridState = rememberLazyGridState() - // Загружаем эмодзи если еще не загружены (синхронно из кеша если уже загружено) - LaunchedEffect(Unit) { + // 🚀 Предзагружаем эмодзи СИНХРОННО при создании компонента + val emojisReady = remember { if (!EmojiCache.isLoaded) { - EmojiCache.loadEmojis(context) + // Загружаем синхронно для мгновенного отображения + runBlocking { + EmojiCache.loadEmojis(context) + } } + true } // Текущие эмодзи для выбранной категории - используем derivedStateOf для оптимизации @@ -631,7 +630,7 @@ fun AppleEmojiPickerPanel( } } - // Сбрасываем скролл при смене категории + // 🚀 Убираем анимацию скролла для мгновенного переключения категорий LaunchedEffect(selectedCategory) { gridState.scrollToItem(0) } @@ -700,6 +699,7 @@ fun AppleEmojiPickerPanel( ) } } else { + // 🚀 Оптимизированная LazyVerticalGrid для быстрого рендеринга LazyVerticalGrid( state = gridState, columns = GridCells.Fixed(8),