diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index cc5291f..275f43d 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -55,6 +55,9 @@ class MainActivity : ComponentActivity() { accountManager = AccountManager(this) RecentSearchesManager.init(this) + // 🔥 Инициализируем ProtocolManager для обработки онлайн статусов + ProtocolManager.initialize(this) + setContent { val scope = rememberCoroutineScope() val isDarkTheme by preferencesManager.isDarkTheme.collectAsState(initial = true) 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 ca435a8..ca2cecc 100644 --- a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt @@ -102,14 +102,11 @@ object ProtocolManager { val onlinePacket = packet as PacketOnlineState addLog("🟢 Online status received: ${onlinePacket.publicKeysState.size} entries") - onlinePacket.publicKeysState.forEach { item -> - addLog(" User ${item.publicKey.take(16)}... is ${item.state}") - - scope.launch { - messageRepository?.updateOnlineStatus( - publicKey = item.publicKey, - isOnline = item.state == OnlineState.ONLINE - ) + 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) } } } @@ -126,20 +123,6 @@ object ProtocolManager { _typingUsers.value = _typingUsers.value - typingPacket.fromPublicKey } } - - // 🟢 Обработчик онлайн статуса (0x05) - waitPacket(0x05) { packet -> - val onlinePacket = packet as PacketOnlineState - 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) - } - } - } } /** 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 fd9981d..7adebc7 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 @@ -648,56 +648,80 @@ fun ChatDetailScreen( ) } - // Выпадающее меню + // Выпадающее меню - чистый дизайн без артефактов DropdownMenu( expanded = showMenu, onDismissRequest = { showMenu = false }, modifier = Modifier + .width(220.dp) + .clip(RoundedCornerShape(16.dp)) .background( - color = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, - shape = RoundedCornerShape(12.dp) + color = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White + ) + .shadow( + elevation = 8.dp, + shape = RoundedCornerShape(16.dp) ) ) { - // Delete Chat + // Delete Chat - красный DropdownMenuItem( text = { - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 4.dp) + ) { Icon( Icons.Default.Delete, contentDescription = null, - tint = PrimaryBlue, - modifier = Modifier.size(20.dp) + tint = Color(0xFFE53935), + modifier = Modifier.size(22.dp) ) - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(14.dp)) Text( "Delete Chat", - color = textColor, - fontSize = 16.sp + color = Color(0xFFE53935), + fontSize = 16.sp, + fontWeight = FontWeight.Medium ) } }, onClick = { showMenu = false showDeleteConfirm = true - } + }, + modifier = Modifier.padding(horizontal = 8.dp) ) - // Block/Unblock User (не показываем для Saved Messages) + // Разделитель + if (!isSavedMessages) { + Divider( + modifier = Modifier.padding(horizontal = 16.dp), + thickness = 0.5.dp, + color = if (isDarkTheme) Color.White.copy(alpha = 0.1f) + else Color.Black.copy(alpha = 0.08f) + ) + } + + // Block/Unblock User - синий (не показываем для Saved Messages) if (!isSavedMessages) { DropdownMenuItem( text = { - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 4.dp) + ) { Icon( if (isBlocked) Icons.Default.Check else Icons.Default.Block, contentDescription = null, - tint = if (isBlocked) PrimaryBlue else Color(0xFFFF3B30), - modifier = Modifier.size(20.dp) + tint = PrimaryBlue, + modifier = Modifier.size(22.dp) ) - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(14.dp)) Text( if (isBlocked) "Unblock User" else "Block User", - color = if (isBlocked) PrimaryBlue else Color(0xFFFF3B30), - fontSize = 16.sp + color = PrimaryBlue, + fontSize = 16.sp, + fontWeight = FontWeight.Medium ) } }, @@ -708,32 +732,46 @@ fun ChatDetailScreen( } else { showBlockConfirm = true } - } + }, + modifier = Modifier.padding(horizontal = 8.dp) + ) + + // Разделитель + Divider( + modifier = Modifier.padding(horizontal = 16.dp), + thickness = 0.5.dp, + color = if (isDarkTheme) Color.White.copy(alpha = 0.1f) + else Color.Black.copy(alpha = 0.08f) ) } // Debug Logs DropdownMenuItem( text = { - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 4.dp) + ) { Icon( Icons.Default.BugReport, contentDescription = null, tint = secondaryTextColor, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(22.dp) ) - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(14.dp)) Text( "Debug Logs", color = textColor, - fontSize = 16.sp + fontSize = 16.sp, + fontWeight = FontWeight.Medium ) } }, onClick = { showMenu = false showLogs = true - } + }, + modifier = Modifier.padding(horizontal = 8.dp) ) } } @@ -1580,8 +1618,9 @@ private fun MessageBubble( .background(bubbleColor) .padding(horizontal = 12.dp, vertical = 7.dp) ) { + // 🔥 Telegram-style: текст и время на одной строке, выровнены по нижней границе Column { - // 🔥 Reply bubble (цитата) - как в React Native + // Reply bubble (цитата) message.replyData?.let { reply -> ReplyBubble( replyData = reply, @@ -1591,46 +1630,31 @@ private fun MessageBubble( Spacer(modifier = Modifier.height(6.dp)) } - // 🔥 Telegram-style: текст + время inline - if (!message.isOutgoing) { - // Входящие сообщения - время справа inline с текстом - Box { - AppleEmojiText( - text = message.text + " ", // Пробелы для места под время - color = textColor, - fontSize = 16.sp - ) - // Время в правом нижнем углу + // Текст и время в одной строке (Row) + Row( + verticalAlignment = Alignment.Bottom, // Выравнивание по нижней границе (baseline) + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + // Текст (не растягивается на всю ширину) + AppleEmojiText( + text = message.text, + color = textColor, + fontSize = 16.sp, + modifier = Modifier.weight(1f, fill = false) + ) + + // Время и статус справа + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(3.dp), + modifier = Modifier.padding(bottom = 1.dp) // Небольшая коррекция для выравнивания + ) { Text( text = timeFormat.format(message.timestamp), color = timeColor, - fontSize = 11.sp, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(top = 2.dp) + fontSize = 11.sp ) - } - } else { - // Исходящие сообщения - время + статус справа внизу - Box { - AppleEmojiText( - text = message.text + " ", // Пробелы для места под время и статус - color = textColor, - fontSize = 16.sp - ) - // Время и статус в правом нижнем углу - Row( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(top = 2.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = timeFormat.format(message.timestamp), - color = timeColor, - fontSize = 11.sp - ) - Spacer(modifier = Modifier.width(3.dp)) + if (message.isOutgoing) { AnimatedMessageStatus( status = message.status, timeColor = timeColor, 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 25cc7a3..15d22ac 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 @@ -395,12 +395,12 @@ fun ChatsListScreen( topBar = { AnimatedVisibility( visible = visible, - enter = - fadeIn(tween(400)) + - slideInVertically( - initialOffsetY = { -it }, - animationSpec = tween(400) - ) + enter = fadeIn(tween(300)) + expandVertically( + animationSpec = tween(300, easing = FastOutSlowInEasing) + ), + exit = fadeOut(tween(200)) + shrinkVertically( + animationSpec = tween(200) + ) ) { key(isDarkTheme) { TopAppBar( @@ -833,20 +833,21 @@ fun DialogItem(dialog: DialogUiModel, isDarkTheme: Boolean, onClick: () -> Unit) ) } - // Online indicator + // Online indicator - зелёный кружок с белой обводкой if (dialog.isOnline == 1) { Box( modifier = - Modifier.size(16.dp) + Modifier.size(18.dp) .align(Alignment.BottomEnd) + .offset(x = (-2).dp, y = (-2).dp) .clip(CircleShape) .background( - if (isDarkTheme) Color(0xFF1A1A1A) + if (isDarkTheme) Color(0xFF1C1C1E) else Color.White ) - .padding(2.dp) + .padding(3.dp) .clip(CircleShape) - .background(Color(0xFF4CAF50)) + .background(Color(0xFF34C759)) // iOS зелёный цвет ) } }