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 90e10b0..b7d936f 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 @@ -105,7 +105,7 @@ fun SetPasswordScreen( Icon( imageVector = Icons.Default.ArrowBack, contentDescription = "Back", - tint = textColor + tint = textColor.copy(alpha = 0.6f) ) } Spacer(modifier = Modifier.weight(1f)) 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 58d7671..3ae3c19 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 @@ -285,7 +285,7 @@ fun UnlockScreen( Icon( imageVector = Icons.Default.KeyboardArrowDown, contentDescription = null, - tint = secondaryTextColor, + tint = secondaryTextColor.copy(alpha = 0.6f), modifier = Modifier .size(24.dp) .graphicsLayer { @@ -332,7 +332,7 @@ fun UnlockScreen( Icon( Icons.Default.Search, contentDescription = null, - tint = secondaryTextColor + tint = secondaryTextColor.copy(alpha = 0.6f) ) }, colors = OutlinedTextFieldDefaults.colors( 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 6fef30a..b56b6dc 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 @@ -80,7 +80,7 @@ fun WelcomeScreen( Icon( Icons.Default.ArrowBack, contentDescription = "Back", - tint = textColor + tint = textColor.copy(alpha = 0.6f) ) } } 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 e639dbd..82ef059 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 @@ -802,7 +802,7 @@ fun ChatDetailScreen( Icon( Icons.Default.Call, contentDescription = "Call", - tint = headerIconColor + tint = headerIconColor.copy(alpha = 0.6f) ) } } @@ -826,7 +826,7 @@ fun ChatDetailScreen( Icon( Icons.Default.MoreVert, contentDescription = "More", - tint = headerIconColor, + tint = headerIconColor.copy(alpha = 0.6f), modifier = Modifier.size(26.dp) ) } @@ -2480,7 +2480,7 @@ private fun MessageInputBar( Icon( Icons.Default.AttachFile, contentDescription = "Attach", - tint = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93), + tint = if (isDarkTheme) Color(0xFF8E8E93).copy(alpha = 0.6f) else Color(0xFF8E8E93).copy(alpha = 0.6f), modifier = Modifier.size(24.dp) ) } @@ -2534,7 +2534,7 @@ private fun MessageInputBar( if (showEmojiPicker) Icons.Default.Keyboard else Icons.Default.SentimentSatisfiedAlt, contentDescription = "Emoji", - tint = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93), + tint = if (isDarkTheme) Color(0xFF8E8E93).copy(alpha = 0.6f) else Color(0xFF8E8E93).copy(alpha = 0.6f), modifier = Modifier.size(24.dp) ) } 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 a4320b1..6be3609 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 @@ -1,8 +1,10 @@ package com.rosetta.messenger.ui.chats +import android.content.Context import androidx.compose.animation.* import androidx.compose.animation.core.* import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -19,26 +21,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.draw.scale -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.foundation.gestures.detectHorizontalDragGestures -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.platform.LocalContext -import android.content.Context import com.airbnb.lottie.compose.* import com.rosetta.messenger.R import com.rosetta.messenger.data.RecentSearchesManager -import com.rosetta.messenger.database.RosettaDatabase import com.rosetta.messenger.network.ProtocolManager import com.rosetta.messenger.network.ProtocolState import com.rosetta.messenger.ui.components.AppleEmojiText @@ -185,7 +180,7 @@ fun ChatsListScreen( // Protocol connection state val protocolState by ProtocolManager.state.collectAsState() - + // 🔥 Пользователи, которые сейчас печатают val typingUsers by ProtocolManager.typingUsers.collectAsState() @@ -203,23 +198,23 @@ fun ChatsListScreen( // Status dialog state var showStatusDialog by remember { mutableStateOf(false) } val debugLogs by ProtocolManager.debugLogs.collectAsState() - + // � FCM токен диалог var showFcmDialog by remember { mutableStateOf(false) } val context = LocalContext.current - + // �📬 Requests screen state var showRequestsScreen by remember { mutableStateOf(false) } // 🔥 Используем rememberSaveable чтобы сохранить состояние при навигации // Header сразу visible = true, без анимации при возврате из чата var visible by rememberSaveable { mutableStateOf(true) } - + // Confirmation dialogs state var dialogToDelete by remember { mutableStateOf(null) } var dialogToBlock by remember { mutableStateOf(null) } var dialogToUnblock by remember { mutableStateOf(null) } - + // Trigger для обновления статуса блокировки var blocklistUpdateTrigger by remember { mutableStateOf(0) } @@ -239,170 +234,173 @@ fun ChatsListScreen( ) } */ - + // Status dialog with logs if (showStatusDialog) { val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current val scrollState = rememberScrollState() - + AlertDialog( - onDismissRequest = { showStatusDialog = false }, - title = { - Text( - "Connection Status & Logs", - fontWeight = FontWeight.Bold, - color = textColor - ) - }, - text = { - Column( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 500.dp) - ) { - // Status indicator - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp) - ) { - Box( - modifier = Modifier - .size(12.dp) - .clip(CircleShape) - .background( - when (protocolState) { - ProtocolState.AUTHENTICATED -> Color(0xFF4CAF50) - ProtocolState.CONNECTING, ProtocolState.CONNECTED, ProtocolState.HANDSHAKING -> Color(0xFFFFC107) - ProtocolState.DISCONNECTED -> Color(0xFFF44336) - } - ) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = when (protocolState) { - ProtocolState.DISCONNECTED -> "Disconnected" - ProtocolState.CONNECTING -> "Connecting..." - ProtocolState.CONNECTED -> "Connected" - ProtocolState.HANDSHAKING -> "Authenticating..." - ProtocolState.AUTHENTICATED -> "Authenticated" - }, - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - color = textColor - ) - } - - Divider( - color = if (isDarkTheme) Color(0xFF424242) else Color(0xFFE0E0E0), - modifier = Modifier.padding(vertical = 8.dp) - ) - - // Logs header with copy button - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - "Debug Logs:", - fontSize = 14.sp, + onDismissRequest = { showStatusDialog = false }, + title = { + Text( + "Connection Status & Logs", fontWeight = FontWeight.Bold, - color = secondaryTextColor - ) - TextButton( - onClick = { - val logsText = debugLogs.joinToString("\n") - clipboardManager.setText(androidx.compose.ui.text.AnnotatedString(logsText)) - android.widget.Toast.makeText( - context, - "Logs copied to clipboard!", - android.widget.Toast.LENGTH_SHORT - ).show() - }, - enabled = debugLogs.isNotEmpty() + color = textColor + ) + }, + text = { + Column(modifier = Modifier.fillMaxWidth().heightIn(max = 500.dp)) { + // Status indicator + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp) ) { + Box( + modifier = + Modifier.size(12.dp) + .clip(CircleShape) + .background( + when (protocolState) { + ProtocolState.AUTHENTICATED -> + Color(0xFF4CAF50) + ProtocolState.CONNECTING, + ProtocolState.CONNECTED, + ProtocolState.HANDSHAKING -> + Color(0xFFFFC107) + ProtocolState.DISCONNECTED -> + Color(0xFFF44336) + } + ) + ) + Spacer(modifier = Modifier.width(12.dp)) Text( - "Copy All", - fontSize = 12.sp, - color = if (debugLogs.isNotEmpty()) PrimaryBlue else Color.Gray + text = + when (protocolState) { + ProtocolState.DISCONNECTED -> "Disconnected" + ProtocolState.CONNECTING -> "Connecting..." + ProtocolState.CONNECTED -> "Connected" + ProtocolState.HANDSHAKING -> "Authenticating..." + ProtocolState.AUTHENTICATED -> "Authenticated" + }, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = textColor ) } - } - - // Logs content - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f, fill = false) - .clip(RoundedCornerShape(8.dp)) - .background(if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF5F5F5)) - .padding(8.dp) - ) { - if (debugLogs.isEmpty()) { + + Divider( + color = if (isDarkTheme) Color(0xFF424242) else Color(0xFFE0E0E0), + modifier = Modifier.padding(vertical = 8.dp) + ) + + // Logs header with copy button + Row( + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { Text( - "No logs available.\nLogs are disabled by default for performance.\n\nEnable with:\nProtocolManager.enableUILogs(true)", - fontSize = 12.sp, - color = secondaryTextColor, - modifier = Modifier.padding(8.dp) + "Debug Logs:", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = secondaryTextColor ) - } else { - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(scrollState) + TextButton( + onClick = { + val logsText = debugLogs.joinToString("\n") + clipboardManager.setText( + androidx.compose.ui.text.AnnotatedString(logsText) + ) + android.widget.Toast.makeText( + context, + "Logs copied to clipboard!", + android.widget.Toast.LENGTH_SHORT + ) + .show() + }, + enabled = debugLogs.isNotEmpty() ) { - debugLogs.forEach { log -> - Text( - log, - fontSize = 11.sp, - fontFamily = FontFamily.Monospace, - color = textColor, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 2.dp) - ) + Text( + "Copy All", + fontSize = 12.sp, + color = + if (debugLogs.isNotEmpty()) PrimaryBlue + else Color.Gray + ) + } + } + + // Logs content + Box( + modifier = + Modifier.fillMaxWidth() + .weight(1f, fill = false) + .clip(RoundedCornerShape(8.dp)) + .background( + if (isDarkTheme) Color(0xFF1A1A1A) + else Color(0xFFF5F5F5) + ) + .padding(8.dp) + ) { + if (debugLogs.isEmpty()) { + Text( + "No logs available.\nLogs are disabled by default for performance.\n\nEnable with:\nProtocolManager.enableUILogs(true)", + fontSize = 12.sp, + color = secondaryTextColor, + modifier = Modifier.padding(8.dp) + ) + } else { + Column( + modifier = + Modifier.fillMaxWidth().verticalScroll(scrollState) + ) { + debugLogs.forEach { log -> + Text( + log, + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + color = textColor, + modifier = + Modifier.fillMaxWidth() + .padding(vertical = 2.dp) + ) + } } } } + + // Enable/Disable logs button + TextButton( + onClick = { + ProtocolManager.enableUILogs(!debugLogs.isNotEmpty()) + android.widget.Toast.makeText( + context, + if (debugLogs.isEmpty()) "Logs enabled" + else "Logs disabled", + android.widget.Toast.LENGTH_SHORT + ) + .show() + }, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp) + ) { + Text( + if (debugLogs.isEmpty()) "⚠️ Enable Logs" else "Disable Logs", + fontSize = 12.sp, + color = + if (debugLogs.isEmpty()) Color(0xFFFFC107) + else Color.Gray + ) + } } - - // Enable/Disable logs button - TextButton( - onClick = { - ProtocolManager.enableUILogs(!debugLogs.isNotEmpty()) - android.widget.Toast.makeText( - context, - if (debugLogs.isEmpty()) "Logs enabled" else "Logs disabled", - android.widget.Toast.LENGTH_SHORT - ).show() - }, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp) - ) { - Text( - if (debugLogs.isEmpty()) "⚠️ Enable Logs" else "Disable Logs", - fontSize = 12.sp, - color = if (debugLogs.isEmpty()) Color(0xFFFFC107) else Color.Gray - ) - } - } - }, - confirmButton = { - Button( - onClick = { showStatusDialog = false }, - colors = ButtonDefaults.buttonColors( - containerColor = PrimaryBlue - ) - ) { - Text("Close", color = Color.White) - } - }, - containerColor = if (isDarkTheme) Color(0xFF212121) else Color.White + }, + confirmButton = { + Button( + onClick = { showStatusDialog = false }, + colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue) + ) { Text("Close", color = Color.White) } + }, + containerColor = if (isDarkTheme) Color(0xFF212121) else Color.White ) } @@ -413,53 +411,60 @@ fun ChatsListScreen( drawerContent = { ModalDrawerSheet( drawerContainerColor = Color.Transparent, - windowInsets = WindowInsets(0), // 🎨 Убираем системные отступы - drawer идет до верха + windowInsets = + WindowInsets( + 0 + ), // 🎨 Убираем системные отступы - drawer идет до верха modifier = Modifier.width(300.dp) ) { Column( - modifier = Modifier - .fillMaxSize() - .background(drawerBackgroundColor) + modifier = Modifier.fillMaxSize().background(drawerBackgroundColor) ) { // ═══════════════════════════════════════════════════════════ // 🎨 DRAWER HEADER - Avatar and status // ═══════════════════════════════════════════════════════════ - val headerColor = if (isDarkTheme) { - Color(0xFF2C5282) - } else { - Color(0xFF4A90D9) - } - + val headerColor = + if (isDarkTheme) { + Color(0xFF2C5282) + } else { + Color(0xFF4A90D9) + } + Box( - modifier = Modifier - .fillMaxWidth() - .background(color = headerColor) - .statusBarsPadding() // 🎨 Контент начинается после status bar - .padding( - top = 16.dp, - start = 20.dp, - end = 20.dp, - bottom = 20.dp - ) + modifier = + Modifier.fillMaxWidth() + .background(color = headerColor) + .statusBarsPadding() // 🎨 Контент начинается + // после status bar + .padding( + top = 16.dp, + start = 20.dp, + end = 20.dp, + bottom = 20.dp + ) ) { Column { // Avatar with border val avatarColors = getAvatarColor(accountPublicKey, isDarkTheme) Box( - modifier = Modifier - .size(72.dp) - .clip(CircleShape) - .background(Color.White.copy(alpha = 0.2f)) - .padding(3.dp) - .clip(CircleShape) - .background(avatarColors.backgroundColor), - contentAlignment = Alignment.Center + modifier = + Modifier.size(72.dp) + .clip(CircleShape) + .background( + Color.White.copy(alpha = 0.2f) + ) + .padding(3.dp) + .clip(CircleShape) + .background( + avatarColors.backgroundColor + ), + contentAlignment = Alignment.Center ) { Text( - text = getAvatarText(accountPublicKey), - fontSize = 26.sp, - fontWeight = FontWeight.Bold, - color = avatarColors.textColor + text = getAvatarText(accountPublicKey), + fontSize = 26.sp, + fontWeight = FontWeight.Bold, + color = avatarColors.textColor ) } @@ -467,40 +472,58 @@ fun ChatsListScreen( // Public key (username style) - clickable для копирования val truncatedKey = - if (accountPublicKey.length > 16) { - "${accountPublicKey.take(8)}...${accountPublicKey.takeLast(6)}" - } else accountPublicKey - + if (accountPublicKey.length > 16) { + "${accountPublicKey.take(8)}...${accountPublicKey.takeLast(6)}" + } else accountPublicKey + 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)) - }, - label = "copiedAnimation" + targetState = showCopiedToast, + transitionSpec = { + fadeIn(animationSpec = tween(300)) togetherWith + fadeOut(animationSpec = tween(300)) + }, + label = "copiedAnimation" ) { isCopied -> Text( - text = if (isCopied) "Copied!" else truncatedKey, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - color = Color.White, - fontStyle = if (isCopied) androidx.compose.ui.text.font.FontStyle.Italic else androidx.compose.ui.text.font.FontStyle.Normal, - modifier = Modifier.clickable { - if (!showCopiedToast) { - // Копируем публичный ключ - val clipboard = context.getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager - val clip = android.content.ClipData.newPlainText("Public Key", accountPublicKey) - clipboard.setPrimaryClip(clip) - showCopiedToast = true - } - } + text = if (isCopied) "Copied!" else truncatedKey, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = Color.White, + fontStyle = + if (isCopied) + androidx.compose.ui.text.font + .FontStyle.Italic + else + androidx.compose.ui.text.font + .FontStyle.Normal, + modifier = + Modifier.clickable { + if (!showCopiedToast) { + // Копируем публичный ключ + val clipboard = + context.getSystemService( + android.content + .Context + .CLIPBOARD_SERVICE + ) as + android.content.ClipboardManager + val clip = + android.content.ClipData + .newPlainText( + "Public Key", + accountPublicKey + ) + clipboard.setPrimaryClip(clip) + showCopiedToast = true + } + } ) } - + // Автоматически возвращаем обратно через 1.5 секунды if (showCopiedToast) { LaunchedEffect(Unit) { @@ -508,36 +531,43 @@ fun ChatsListScreen( showCopiedToast = false } } - + Spacer(modifier = Modifier.height(6.dp)) - + // Connection status indicator Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.clickable { showStatusDialog = true } + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.clickable { showStatusDialog = true } ) { - val statusColor = when (protocolState) { - ProtocolState.AUTHENTICATED -> Color(0xFF4ADE80) - ProtocolState.CONNECTING, ProtocolState.CONNECTED, ProtocolState.HANDSHAKING -> Color(0xFFFBBF24) - else -> Color(0xFFF87171) - } - val statusText = when (protocolState) { - ProtocolState.AUTHENTICATED -> "Online" - ProtocolState.CONNECTING, ProtocolState.CONNECTED, ProtocolState.HANDSHAKING -> "Connecting..." - else -> "Offline" - } - + val statusColor = + when (protocolState) { + ProtocolState.AUTHENTICATED -> Color(0xFF4ADE80) + ProtocolState.CONNECTING, + ProtocolState.CONNECTED, + ProtocolState.HANDSHAKING -> Color(0xFFFBBF24) + else -> Color(0xFFF87171) + } + val statusText = + when (protocolState) { + ProtocolState.AUTHENTICATED -> "Online" + ProtocolState.CONNECTING, + ProtocolState.CONNECTED, + ProtocolState.HANDSHAKING -> "Connecting..." + else -> "Offline" + } + Box( - modifier = Modifier - .size(8.dp) - .clip(CircleShape) - .background(statusColor) + modifier = + Modifier.size(8.dp) + .clip(CircleShape) + .background(statusColor) ) Spacer(modifier = Modifier.width(6.dp)) Text( - text = statusText, - fontSize = 13.sp, - color = Color.White.copy(alpha = 0.85f) + text = statusText, + fontSize = 13.sp, + color = Color.White.copy(alpha = 0.85f) ) } } @@ -547,152 +577,159 @@ fun ChatsListScreen( // 📱 MENU ITEMS // ═══════════════════════════════════════════════════════════ Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .verticalScroll(rememberScrollState()) - .padding(vertical = 8.dp) + modifier = + Modifier.fillMaxWidth() + .weight(1f) + .verticalScroll(rememberScrollState()) + .padding(vertical = 8.dp) ) { - val menuIconColor = if (isDarkTheme) Color(0xFFB0B0B0) else Color(0xFF5C5C5C) - + val menuIconColor = textColor.copy(alpha = 0.6f) + // 👤 Profile Section DrawerMenuItemEnhanced( - icon = Icons.Outlined.Person, - text = "My Profile", - iconColor = menuIconColor, - textColor = textColor, - onClick = { - scope.launch { drawerState.close() } - onProfileClick() - } + icon = Icons.Outlined.Person, + text = "My Profile", + iconColor = menuIconColor, + textColor = textColor, + onClick = { + scope.launch { drawerState.close() } + onProfileClick() + } ) // 📖 Saved Messages DrawerMenuItemEnhanced( - icon = Icons.Outlined.Bookmark, - text = "Saved Messages", - iconColor = menuIconColor, - textColor = textColor, - onClick = { - scope.launch { drawerState.close() } - onSavedMessagesClick() - } + icon = Icons.Outlined.Bookmark, + text = "Saved Messages", + iconColor = menuIconColor, + textColor = textColor, + onClick = { + scope.launch { drawerState.close() } + onSavedMessagesClick() + } ) DrawerDivider(isDarkTheme) // 👥 Contacts DrawerMenuItemEnhanced( - icon = Icons.Outlined.Contacts, - text = "Contacts", - iconColor = menuIconColor, - textColor = textColor, - onClick = { - scope.launch { drawerState.close() } - onContactsClick() - } + icon = Icons.Outlined.Contacts, + text = "Contacts", + iconColor = menuIconColor, + textColor = textColor, + onClick = { + scope.launch { drawerState.close() } + onContactsClick() + } ) // 📞 Calls DrawerMenuItemEnhanced( - icon = Icons.Outlined.Call, - text = "Calls", - iconColor = menuIconColor, - textColor = textColor, - onClick = { - scope.launch { drawerState.close() } - onCallsClick() - } + icon = Icons.Outlined.Call, + text = "Calls", + iconColor = menuIconColor, + textColor = textColor, + onClick = { + scope.launch { drawerState.close() } + onCallsClick() + } ) // ➕ Invite Friends DrawerMenuItemEnhanced( - icon = Icons.Outlined.PersonAdd, - text = "Invite Friends", - iconColor = menuIconColor, - textColor = textColor, - onClick = { - scope.launch { drawerState.close() } - onInviteFriendsClick() - } + icon = Icons.Outlined.PersonAdd, + text = "Invite Friends", + iconColor = menuIconColor, + textColor = textColor, + onClick = { + scope.launch { drawerState.close() } + onInviteFriendsClick() + } ) DrawerDivider(isDarkTheme) // ⚙️ Settings DrawerMenuItemEnhanced( - icon = Icons.Outlined.Settings, - text = "Settings", - iconColor = menuIconColor, - textColor = textColor, - onClick = { - scope.launch { drawerState.close() } - onSettingsClick() - } + icon = Icons.Outlined.Settings, + text = "Settings", + iconColor = menuIconColor, + textColor = textColor, + onClick = { + scope.launch { drawerState.close() } + onSettingsClick() + } ) // 🌓 Theme Toggle DrawerMenuItemEnhanced( - icon = if (isDarkTheme) Icons.Outlined.LightMode else Icons.Outlined.DarkMode, - text = if (isDarkTheme) "Light Mode" else "Dark Mode", - iconColor = menuIconColor, - textColor = textColor, - onClick = { onToggleTheme() } + icon = + if (isDarkTheme) Icons.Outlined.LightMode + else Icons.Outlined.DarkMode, + text = if (isDarkTheme) "Light Mode" else "Dark Mode", + iconColor = menuIconColor, + textColor = textColor, + onClick = { onToggleTheme() } ) // ❓ Help DrawerMenuItemEnhanced( - icon = Icons.Outlined.HelpOutline, - text = "Help & FAQ", - iconColor = menuIconColor, - textColor = textColor, - onClick = { - scope.launch { drawerState.close() } - // TODO: Add help screen navigation - } + icon = Icons.Outlined.HelpOutline, + text = "Help & FAQ", + iconColor = menuIconColor, + textColor = textColor, + onClick = { + scope.launch { drawerState.close() } + // TODO: Add help screen navigation + } ) } // ═══════════════════════════════════════════════════════════ // 🚪 FOOTER - Logout & Version // ═══════════════════════════════════════════════════════════ - Column( - modifier = Modifier.fillMaxWidth() - ) { + Column(modifier = Modifier.fillMaxWidth()) { Divider( - color = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE8E8E8), - thickness = 0.5.dp + color = + if (isDarkTheme) Color(0xFF2A2A2A) + else Color(0xFFE8E8E8), + thickness = 0.5.dp ) - + // Logout DrawerMenuItemEnhanced( - icon = Icons.Outlined.Logout, - text = "Log Out", - iconColor = Color(0xFFFF4444), - textColor = Color(0xFFFF4444), - onClick = { - scope.launch { - drawerState.close() - kotlinx.coroutines.delay(150) - onLogout() + icon = Icons.Outlined.Logout, + text = "Log Out", + iconColor = Color(0xFFFF4444), + textColor = Color(0xFFFF4444), + onClick = { + scope.launch { + drawerState.close() + kotlinx.coroutines.delay(150) + onLogout() + } } - } ) // Version info Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 12.dp), - contentAlignment = Alignment.CenterStart + modifier = + Modifier.fillMaxWidth() + .padding( + horizontal = 20.dp, + vertical = 12.dp + ), + contentAlignment = Alignment.CenterStart ) { Text( - text = "Rosetta v1.0.0", - fontSize = 12.sp, - color = if (isDarkTheme) Color(0xFF666666) else Color(0xFF999999) + text = "Rosetta v1.0.0", + fontSize = 12.sp, + color = + if (isDarkTheme) Color(0xFF666666) + else Color(0xFF999999) ) } - + Spacer(modifier = Modifier.height(16.dp)) } } @@ -703,19 +740,27 @@ fun ChatsListScreen( topBar = { AnimatedVisibility( visible = visible, - enter = fadeIn(tween(300)) + expandVertically( - animationSpec = tween(300, easing = FastOutSlowInEasing) - ), - exit = fadeOut(tween(200)) + shrinkVertically( - animationSpec = tween(200) - ) + enter = + fadeIn(tween(300)) + + expandVertically( + animationSpec = + tween( + 300, + easing = FastOutSlowInEasing + ) + ), + exit = + fadeOut(tween(200)) + + shrinkVertically(animationSpec = tween(200)) ) { key(isDarkTheme, showRequestsScreen) { TopAppBar( navigationIcon = { if (showRequestsScreen) { // Back button for Requests - IconButton(onClick = { showRequestsScreen = false }) { + IconButton( + onClick = { showRequestsScreen = false } + ) { Icon( Icons.Default.ArrowBack, contentDescription = "Back", @@ -732,7 +777,7 @@ fun ChatsListScreen( Icon( Icons.Default.Menu, contentDescription = "Menu", - tint = textColor + tint = textColor.copy(alpha = 0.6f) ) } } @@ -749,7 +794,8 @@ fun ChatsListScreen( } else { // Rosetta title with status Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = + Alignment.CenterVertically ) { Text( "Rosetta", @@ -759,17 +805,37 @@ fun ChatsListScreen( ) Spacer(modifier = Modifier.width(8.dp)) Box( - modifier = Modifier - .size(10.dp) - .clip(CircleShape) - .background( - when (protocolState) { - ProtocolState.AUTHENTICATED -> Color(0xFF4CAF50) - ProtocolState.CONNECTING, ProtocolState.CONNECTED, ProtocolState.HANDSHAKING -> Color(0xFFFFC107) - ProtocolState.DISCONNECTED -> Color(0xFFF44336) + modifier = + Modifier.size(10.dp) + .clip(CircleShape) + .background( + when (protocolState + ) { + ProtocolState + .AUTHENTICATED -> + Color( + 0xFF4CAF50 + ) + ProtocolState + .CONNECTING, + ProtocolState + .CONNECTED, + ProtocolState + .HANDSHAKING -> + Color( + 0xFFFFC107 + ) + ProtocolState + .DISCONNECTED -> + Color( + 0xFFF44336 + ) + } + ) + .clickable { + showStatusDialog = + true } - ) - .clickable { showStatusDialog = true } ) } } @@ -779,28 +845,42 @@ fun ChatsListScreen( if (!showRequestsScreen) { IconButton( onClick = { - if (protocolState == ProtocolState.AUTHENTICATED) { + if (protocolState == + ProtocolState + .AUTHENTICATED + ) { onSearchClick() } }, - enabled = protocolState == ProtocolState.AUTHENTICATED + enabled = + protocolState == + ProtocolState.AUTHENTICATED ) { Icon( Icons.Default.Search, contentDescription = "Search", - tint = if (protocolState == ProtocolState.AUTHENTICATED) - textColor else textColor.copy(alpha = 0.5f) + tint = + if (protocolState == + ProtocolState + .AUTHENTICATED + ) + textColor.copy(alpha = 0.6f) + else + textColor.copy( + alpha = 0.5f + ) ) } } }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = backgroundColor, - scrolledContainerColor = backgroundColor, - navigationIconContentColor = textColor, - titleContentColor = textColor, - actionIconContentColor = textColor - ) + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = backgroundColor, + scrolledContainerColor = backgroundColor, + navigationIconContentColor = textColor, + titleContentColor = textColor, + actionIconContentColor = textColor + ) ) } } @@ -829,50 +909,61 @@ fun ChatsListScreen( // Main content Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) { // � Используем комбинированное состояние для атомарного обновления - // Это предотвращает "дергание" UI когда dialogs и requests обновляются независимо + // Это предотвращает "дергание" UI когда dialogs и requests обновляются + // независимо val chatsState by chatsViewModel.chatsState.collectAsState() val requests = chatsState.requests val requestsCount = chatsState.requestsCount - + // 🎬 Animated content transition between main list and requests AnimatedContent( - targetState = showRequestsScreen, - transitionSpec = { - if (targetState) { - // Переход на Requests: slide in from right + fade - (slideInHorizontally( - animationSpec = tween(300, easing = FastOutSlowInEasing), - initialOffsetX = { it } - ) + fadeIn(tween(300))) togetherWith - (slideOutHorizontally( - animationSpec = tween(300, easing = FastOutSlowInEasing), - targetOffsetX = { -it / 3 } - ) + fadeOut(tween(200))) - } else { - // Возврат из Requests: slide out to right - (slideInHorizontally( - animationSpec = tween(300, easing = FastOutSlowInEasing), - initialOffsetX = { -it / 3 } - ) + fadeIn(tween(300))) togetherWith - (slideOutHorizontally( - animationSpec = tween(300, easing = FastOutSlowInEasing), - targetOffsetX = { it } - ) + fadeOut(tween(200))) - } - }, - label = "RequestsTransition" + targetState = showRequestsScreen, + transitionSpec = { + if (targetState) { + // Переход на Requests: slide in from right + fade + (slideInHorizontally( + animationSpec = + tween(300, easing = FastOutSlowInEasing), + initialOffsetX = { it } + ) + fadeIn(tween(300))) togetherWith + (slideOutHorizontally( + animationSpec = + tween( + 300, + easing = FastOutSlowInEasing + ), + targetOffsetX = { -it / 3 } + ) + fadeOut(tween(200))) + } else { + // Возврат из Requests: slide out to right + (slideInHorizontally( + animationSpec = + tween(300, easing = FastOutSlowInEasing), + initialOffsetX = { -it / 3 } + ) + fadeIn(tween(300))) togetherWith + (slideOutHorizontally( + animationSpec = + tween( + 300, + easing = FastOutSlowInEasing + ), + targetOffsetX = { it } + ) + fadeOut(tween(200))) + } + }, + label = "RequestsTransition" ) { isRequestsScreen -> if (isRequestsScreen) { // 📬 Show Requests Screen RequestsScreen( - requests = requests, - isDarkTheme = isDarkTheme, - onBack = { showRequestsScreen = false }, - onRequestClick = { request -> - showRequestsScreen = false - val user = chatsViewModel.dialogToSearchUser(request) - onUserSelect(user) - } + requests = requests, + isDarkTheme = isDarkTheme, + onBack = { showRequestsScreen = false }, + onRequestClick = { request -> + showRequestsScreen = false + val user = chatsViewModel.dialogToSearchUser(request) + onUserSelect(user) + } ) } else if (chatsState.isEmpty) { // 🔥 Empty state - используем chatsState.isEmpty для атомарной проверки @@ -882,26 +973,24 @@ fun ChatsListScreen( ) } else { // Show dialogs list - val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8) + val dividerColor = + if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8) // 🔥 Берем dialogs из chatsState для консистентности val currentDialogs = chatsState.dialogs - + LazyColumn(modifier = Modifier.fillMaxSize()) { // 📬 Requests Section if (requestsCount > 0) { item(key = "requests_section") { RequestsSection( - count = requestsCount, - isDarkTheme = isDarkTheme, - onClick = { showRequestsScreen = true } - ) - Divider( - color = dividerColor, - thickness = 0.5.dp + count = requestsCount, + isDarkTheme = isDarkTheme, + onClick = { showRequestsScreen = true } ) + Divider(color = dividerColor, thickness = 0.5.dp) } } - + items(currentDialogs, key = { it.opponentKey }) { dialog -> val isSavedMessages = dialog.opponentKey == accountPublicKey // Check if user is blocked @@ -909,7 +998,7 @@ fun ChatsListScreen( LaunchedEffect(dialog.opponentKey, blocklistUpdateTrigger) { isBlocked = chatsViewModel.isUserBlocked(dialog.opponentKey) } - + Column { SwipeableDialogItem( dialog = dialog, @@ -918,25 +1007,22 @@ fun ChatsListScreen( isBlocked = isBlocked, isSavedMessages = isSavedMessages, onClick = { - val user = chatsViewModel.dialogToSearchUser(dialog) + val user = + chatsViewModel.dialogToSearchUser( + dialog + ) onUserSelect(user) }, - onDelete = { - dialogToDelete = dialog - }, - onBlock = { - dialogToBlock = dialog - }, - onUnblock = { - dialogToUnblock = dialog - } + onDelete = { dialogToDelete = dialog }, + onBlock = { dialogToBlock = dialog }, + onUnblock = { dialogToUnblock = dialog } ) - + // 🔥 СЕПАРАТОР - линия разделения между диалогами Divider( - modifier = Modifier.padding(start = 84.dp), - color = dividerColor, - thickness = 0.5.dp + modifier = Modifier.padding(start = 84.dp), + color = dividerColor, + thickness = 0.5.dp ) } } @@ -948,125 +1034,113 @@ fun ChatsListScreen( } } } // Close ModalNavigationDrawer - + // 🔥 Confirmation Dialogs - + // Delete Dialog Confirmation dialogToDelete?.let { dialog -> AlertDialog( - onDismissRequest = { dialogToDelete = null }, - containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, - title = { - Text( - "Delete Chat", - fontWeight = FontWeight.Bold, - color = textColor - ) - }, - text = { - Text( - "Are you sure you want to delete this chat? This action cannot be undone.", - color = secondaryTextColor - ) - }, - confirmButton = { - TextButton( - onClick = { - val opponentKey = dialog.opponentKey - dialogToDelete = null - scope.launch { - chatsViewModel.deleteDialog(opponentKey) - } + onDismissRequest = { dialogToDelete = null }, + containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, + title = { + Text("Delete Chat", fontWeight = FontWeight.Bold, color = textColor) + }, + text = { + Text( + "Are you sure you want to delete this chat? This action cannot be undone.", + color = secondaryTextColor + ) + }, + confirmButton = { + TextButton( + onClick = { + val opponentKey = dialog.opponentKey + dialogToDelete = null + scope.launch { chatsViewModel.deleteDialog(opponentKey) } + } + ) { Text("Delete", color = Color(0xFFFF3B30)) } + }, + dismissButton = { + TextButton(onClick = { dialogToDelete = null }) { + Text("Cancel", color = PrimaryBlue) } - ) { - Text("Delete", color = Color(0xFFFF3B30)) } - }, - dismissButton = { - TextButton(onClick = { dialogToDelete = null }) { - Text("Cancel", color = PrimaryBlue) - } - } ) } - + // Block Dialog Confirmation dialogToBlock?.let { dialog -> AlertDialog( - onDismissRequest = { dialogToBlock = null }, - containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, - title = { - Text( - "Block ${dialog.opponentTitle.ifEmpty { "User" }}", - fontWeight = FontWeight.Bold, - color = textColor - ) - }, - text = { - Text( - "Are you sure you want to block this user? They won't be able to send you messages.", - color = secondaryTextColor - ) - }, - confirmButton = { - TextButton( - onClick = { - val opponentKey = dialog.opponentKey - dialogToBlock = null - scope.launch { - chatsViewModel.blockUser(opponentKey) - blocklistUpdateTrigger++ - } + onDismissRequest = { dialogToBlock = null }, + containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, + title = { + Text( + "Block ${dialog.opponentTitle.ifEmpty { "User" }}", + fontWeight = FontWeight.Bold, + color = textColor + ) + }, + text = { + Text( + "Are you sure you want to block this user? They won't be able to send you messages.", + color = secondaryTextColor + ) + }, + confirmButton = { + TextButton( + onClick = { + val opponentKey = dialog.opponentKey + dialogToBlock = null + scope.launch { + chatsViewModel.blockUser(opponentKey) + blocklistUpdateTrigger++ + } + } + ) { Text("Block", color = Color(0xFFFF3B30)) } + }, + dismissButton = { + TextButton(onClick = { dialogToBlock = null }) { + Text("Cancel", color = PrimaryBlue) } - ) { - Text("Block", color = Color(0xFFFF3B30)) } - }, - dismissButton = { - TextButton(onClick = { dialogToBlock = null }) { - Text("Cancel", color = PrimaryBlue) - } - } ) } - + // Unblock Dialog Confirmation dialogToUnblock?.let { dialog -> AlertDialog( - onDismissRequest = { dialogToUnblock = null }, - containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, - title = { - Text( - "Unblock ${dialog.opponentTitle.ifEmpty { "User" }}", - fontWeight = FontWeight.Bold, - color = textColor - ) - }, - text = { - Text( - "Are you sure you want to unblock this user? They will be able to send you messages again.", - color = secondaryTextColor - ) - }, - confirmButton = { - TextButton( - onClick = { - val opponentKey = dialog.opponentKey - dialogToUnblock = null - scope.launch { - chatsViewModel.unblockUser(opponentKey) - blocklistUpdateTrigger++ - } + onDismissRequest = { dialogToUnblock = null }, + containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, + title = { + Text( + "Unblock ${dialog.opponentTitle.ifEmpty { "User" }}", + fontWeight = FontWeight.Bold, + color = textColor + ) + }, + text = { + Text( + "Are you sure you want to unblock this user? They will be able to send you messages again.", + color = secondaryTextColor + ) + }, + confirmButton = { + TextButton( + onClick = { + val opponentKey = dialog.opponentKey + dialogToUnblock = null + scope.launch { + chatsViewModel.unblockUser(opponentKey) + blocklistUpdateTrigger++ + } + } + ) { Text("Unblock", color = PrimaryBlue) } + }, + dismissButton = { + TextButton(onClick = { dialogToUnblock = null }) { + Text("Cancel", color = Color(0xFF8E8E93)) } - ) { - Text("Unblock", color = PrimaryBlue) } - }, - dismissButton = { - TextButton(onClick = { dialogToUnblock = null }) { - Text("Cancel", color = Color(0xFF8E8E93)) - } - } ) } } // Close Box @@ -1223,7 +1297,7 @@ fun ChatItem(chat: Chat, isDarkTheme: Boolean, onClick: () -> Unit) { Icon( Icons.Default.PushPin, contentDescription = "Pinned", - tint = secondaryTextColor, + tint = secondaryTextColor.copy(alpha = 0.6f), modifier = Modifier.size(16.dp).padding(end = 4.dp) ) } @@ -1324,7 +1398,7 @@ fun DrawerMenuItem( Icon( imageVector = icon, contentDescription = null, - tint = textColor, + tint = textColor.copy(alpha = 0.6f), modifier = Modifier.size(24.dp) ) @@ -1335,158 +1409,152 @@ fun DrawerMenuItem( } /** - * 🔥 Swipeable wrapper для DialogItem с кнопками Block и Delete - * Свайп влево показывает действия (как в React Native версии) + * 🔥 Swipeable wrapper для DialogItem с кнопками Block и Delete Свайп влево показывает действия + * (как в React Native версии) */ @Composable fun SwipeableDialogItem( - dialog: DialogUiModel, - isDarkTheme: Boolean, - isTyping: Boolean = false, - isBlocked: Boolean = false, - isSavedMessages: Boolean = false, - onClick: () -> Unit, - onDelete: () -> Unit = {}, - onBlock: () -> Unit = {}, - onUnblock: () -> Unit = {} + dialog: DialogUiModel, + isDarkTheme: Boolean, + isTyping: Boolean = false, + isBlocked: Boolean = false, + isSavedMessages: Boolean = false, + onClick: () -> Unit, + onDelete: () -> Unit = {}, + onBlock: () -> Unit = {}, + onUnblock: () -> Unit = {} ) { val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF) var offsetX by remember { mutableStateOf(0f) } val swipeWidthDp = if (isSavedMessages) 80.dp else 160.dp val density = androidx.compose.ui.platform.LocalDensity.current val swipeWidthPx = with(density) { swipeWidthDp.toPx() } - + // Фиксированная высота элемента (как в DialogItem) val itemHeight = 80.dp - + // Анимация возврата - val animatedOffsetX by animateFloatAsState( - targetValue = offsetX, - animationSpec = spring(dampingRatio = 0.7f, stiffness = 400f), - label = "swipeOffset" - ) - - Box( - modifier = Modifier - .fillMaxWidth() - .height(itemHeight) - .clipToBounds() - ) { + val animatedOffsetX by + animateFloatAsState( + targetValue = offsetX, + animationSpec = spring(dampingRatio = 0.7f, stiffness = 400f), + label = "swipeOffset" + ) + + Box(modifier = Modifier.fillMaxWidth().height(itemHeight).clipToBounds()) { // 1. КНОПКИ - позиционированы справа, всегда видны при свайпе - Row( - modifier = Modifier - .align(Alignment.CenterEnd) - .height(itemHeight) - .width(swipeWidthDp) - ) { + Row(modifier = Modifier.align(Alignment.CenterEnd).height(itemHeight).width(swipeWidthDp)) { // Кнопка Block/Unblock (только если не Saved Messages) if (!isSavedMessages) { Box( - modifier = Modifier - .width(80.dp) - .fillMaxHeight() - .background(if (isBlocked) Color(0xFF4CAF50) else Color(0xFFFF6B6B)) - .clickable { - if (isBlocked) onUnblock() else onBlock() - offsetX = 0f - }, - contentAlignment = Alignment.Center + modifier = + Modifier.width(80.dp) + .fillMaxHeight() + .background( + if (isBlocked) Color(0xFF4CAF50) + else Color(0xFFFF6B6B) + ) + .clickable { + if (isBlocked) onUnblock() else onBlock() + offsetX = 0f + }, + contentAlignment = Alignment.Center ) { Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { Icon( - imageVector = if (isBlocked) Icons.Default.LockOpen else Icons.Default.Block, - contentDescription = if (isBlocked) "Unblock" else "Block", - tint = Color.White, - modifier = Modifier.size(22.dp) + imageVector = + if (isBlocked) Icons.Default.LockOpen + else Icons.Default.Block, + contentDescription = if (isBlocked) "Unblock" else "Block", + tint = Color.White, + modifier = Modifier.size(22.dp) ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = if (isBlocked) "Unblock" else "Block", - color = Color.White, - fontSize = 12.sp, - fontWeight = FontWeight.SemiBold + text = if (isBlocked) "Unblock" else "Block", + color = Color.White, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold ) } } } - + // Кнопка Delete Box( - modifier = Modifier - .width(80.dp) - .fillMaxHeight() - .background(PrimaryBlue) - .clickable { - // Закрываем свайп мгновенно перед удалением - offsetX = 0f - onDelete() - }, - contentAlignment = Alignment.Center + modifier = + Modifier.width(80.dp) + .fillMaxHeight() + .background(PrimaryBlue) + .clickable { + // Закрываем свайп мгновенно перед удалением + offsetX = 0f + onDelete() + }, + contentAlignment = Alignment.Center ) { Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { Icon( - imageVector = Icons.Default.Delete, - contentDescription = "Delete", - tint = Color.White, - modifier = Modifier.size(22.dp) + imageVector = Icons.Default.Delete, + contentDescription = "Delete", + tint = Color.White, + modifier = Modifier.size(22.dp) ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = "Delete", - color = Color.White, - fontSize = 12.sp, - fontWeight = FontWeight.SemiBold + text = "Delete", + color = Color.White, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold ) } } } - + // 2. КОНТЕНТ - поверх кнопок, сдвигается при свайпе Column( - modifier = Modifier - .fillMaxSize() - .offset { IntOffset(animatedOffsetX.toInt(), 0) } - .background(backgroundColor) - .pointerInput(Unit) { - detectHorizontalDragGestures( - onDragEnd = { - // Если свайпнули больше чем на половину - фиксируем - if (kotlin.math.abs(offsetX) > swipeWidthPx / 2) { - offsetX = -swipeWidthPx - } else { - offsetX = 0f - } - }, - onDragCancel = { - offsetX = 0f - }, - onHorizontalDrag = { _, dragAmount -> - // Только свайп влево (отрицательное значение) - val newOffset = offsetX + dragAmount - offsetX = newOffset.coerceIn(-swipeWidthPx, 0f) - } - ) - } + modifier = + Modifier.fillMaxSize() + .offset { IntOffset(animatedOffsetX.toInt(), 0) } + .background(backgroundColor) + .pointerInput(Unit) { + detectHorizontalDragGestures( + onDragEnd = { + // Если свайпнули больше чем на половину - фиксируем + if (kotlin.math.abs(offsetX) > swipeWidthPx / 2) { + offsetX = -swipeWidthPx + } else { + offsetX = 0f + } + }, + onDragCancel = { offsetX = 0f }, + onHorizontalDrag = { _, dragAmount -> + // Только свайп влево (отрицательное значение) + val newOffset = offsetX + dragAmount + offsetX = newOffset.coerceIn(-swipeWidthPx, 0f) + } + ) + } ) { DialogItemContent( - dialog = dialog, - isDarkTheme = isDarkTheme, - isTyping = isTyping, - onClick = onClick + dialog = dialog, + isDarkTheme = isDarkTheme, + isTyping = isTyping, + onClick = onClick ) - + // Сепаратор внутри контента val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8) Divider( - modifier = Modifier.padding(start = 84.dp), - color = dividerColor, - thickness = 0.5.dp + modifier = Modifier.padding(start = 84.dp), + color = dividerColor, + thickness = 0.5.dp ) } } @@ -1494,26 +1562,37 @@ fun SwipeableDialogItem( /** Элемент диалога из базы данных - ОПТИМИЗИРОВАННЫЙ (без сепаратора для SwipeableDialogItem) */ @Composable -fun DialogItemContent(dialog: DialogUiModel, isDarkTheme: Boolean, isTyping: Boolean = false, onClick: () -> Unit) { +fun DialogItemContent( + dialog: DialogUiModel, + isDarkTheme: Boolean, + isTyping: Boolean = false, + onClick: () -> Unit +) { // 🔥 ОПТИМИЗАЦИЯ: Кешируем цвета и строки val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black } - val secondaryTextColor = remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) } + val secondaryTextColor = + remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) } - val avatarColors = remember(dialog.opponentKey, isDarkTheme) { getAvatarColor(dialog.opponentKey, isDarkTheme) } - val displayName = remember(dialog.opponentTitle, dialog.opponentKey) { - dialog.opponentTitle.ifEmpty { dialog.opponentKey.take(8) } - } - val initials = remember(dialog.opponentTitle, dialog.opponentKey) { - if (dialog.opponentTitle.isNotEmpty()) { - dialog.opponentTitle - .split(" ") - .take(2) - .mapNotNull { it.firstOrNull()?.uppercase() } - .joinToString("") - } else { - dialog.opponentKey.take(2).uppercase() - } - } + val avatarColors = + remember(dialog.opponentKey, isDarkTheme) { + getAvatarColor(dialog.opponentKey, isDarkTheme) + } + val displayName = + remember(dialog.opponentTitle, dialog.opponentKey) { + dialog.opponentTitle.ifEmpty { dialog.opponentKey.take(8) } + } + val initials = + remember(dialog.opponentTitle, dialog.opponentKey) { + if (dialog.opponentTitle.isNotEmpty()) { + dialog.opponentTitle + .split(" ") + .take(2) + .mapNotNull { it.firstOrNull()?.uppercase() } + .joinToString("") + } else { + dialog.opponentKey.take(2).uppercase() + } + } Row( modifier = @@ -1532,247 +1611,232 @@ fun DialogItemContent(dialog: DialogUiModel, isDarkTheme: Boolean, isTyping: Boo .background(avatarColors.backgroundColor), contentAlignment = Alignment.Center ) { - Text( - text = initials, - color = avatarColors.textColor, - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp - ) - } - - // Online indicator - зелёный кружок с белой обводкой - if (dialog.isOnline == 1) { - Box( - modifier = - Modifier.size(18.dp) - .align(Alignment.BottomEnd) - .offset(x = (-2).dp, y = (-2).dp) - .clip(CircleShape) - .background( - if (isDarkTheme) Color(0xFF1C1C1E) - else Color.White - ) - .padding(3.dp) - .clip(CircleShape) - .background(Color(0xFF34C759)) // iOS зелёный цвет - ) - } + Text( + text = initials, + color = avatarColors.textColor, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp + ) } - Spacer(modifier = Modifier.width(12.dp)) + // Online indicator - зелёный кружок с белой обводкой + if (dialog.isOnline == 1) { + Box( + modifier = + Modifier.size(18.dp) + .align(Alignment.BottomEnd) + .offset(x = (-2).dp, y = (-2).dp) + .clip(CircleShape) + .background( + if (isDarkTheme) Color(0xFF1C1C1E) else Color.White + ) + .padding(3.dp) + .clip(CircleShape) + .background(Color(0xFF34C759)) // iOS зелёный цвет + ) + } + } - // Name and last message - Column(modifier = Modifier.weight(1f)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = displayName, - fontWeight = FontWeight.SemiBold, - fontSize = 16.sp, - color = textColor, + Spacer(modifier = Modifier.width(12.dp)) + + // Name and last message + Column(modifier = Modifier.weight(1f)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = displayName, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + color = textColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + + Text( + text = formatTime(Date(dialog.lastMessageTimestamp)), + fontSize = 13.sp, + color = if (dialog.unreadCount > 0) PrimaryBlue else secondaryTextColor + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + // 🔥 Показываем typing индикатор или последнее сообщение + if (isTyping) { + TypingIndicatorSmall() + } else { + // 🔥 Используем AppleEmojiText для отображения эмодзи + // Если есть непрочитанные - текст темнее + AppleEmojiText( + text = dialog.lastMessage.ifEmpty { "No messages" }, + fontSize = 14.sp, + color = + if (dialog.unreadCount > 0) textColor.copy(alpha = 0.85f) + else secondaryTextColor, + fontWeight = + if (dialog.unreadCount > 0) FontWeight.Medium + else FontWeight.Normal, maxLines = 1, - overflow = TextOverflow.Ellipsis, + overflow = android.text.TextUtils.TruncateAt.END, modifier = Modifier.weight(1f) ) - - Text( - text = formatTime(Date(dialog.lastMessageTimestamp)), - fontSize = 13.sp, - color = if (dialog.unreadCount > 0) PrimaryBlue else secondaryTextColor - ) } - Spacer(modifier = Modifier.height(4.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - // 🔥 Показываем typing индикатор или последнее сообщение - if (isTyping) { - TypingIndicatorSmall() - } else { - // 🔥 Используем AppleEmojiText для отображения эмодзи - // Если есть непрочитанные - текст темнее - AppleEmojiText( - text = dialog.lastMessage.ifEmpty { "No messages" }, - fontSize = 14.sp, - color = if (dialog.unreadCount > 0) textColor.copy(alpha = 0.85f) else secondaryTextColor, - fontWeight = if (dialog.unreadCount > 0) FontWeight.Medium else FontWeight.Normal, - maxLines = 1, - overflow = android.text.TextUtils.TruncateAt.END, - modifier = Modifier.weight(1f) + // Unread badge + if (dialog.unreadCount > 0) { + Spacer(modifier = Modifier.width(8.dp)) + val unreadText = + when { + dialog.unreadCount > 999 -> "999+" + dialog.unreadCount > 99 -> "99+" + else -> dialog.unreadCount.toString() + } + Box( + modifier = + Modifier.height(22.dp) + .widthIn(min = 22.dp) + .clip(RoundedCornerShape(11.dp)) + .background(PrimaryBlue) + .padding(horizontal = 6.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = unreadText, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + maxLines = 1 ) } - - // Unread badge - if (dialog.unreadCount > 0) { - Spacer(modifier = Modifier.width(8.dp)) - val unreadText = when { - dialog.unreadCount > 999 -> "999+" - dialog.unreadCount > 99 -> "99+" - else -> dialog.unreadCount.toString() - } - Box( - modifier = - Modifier.height(22.dp) - .widthIn(min = 22.dp) - .clip(RoundedCornerShape(11.dp)) - .background(PrimaryBlue) - .padding(horizontal = 6.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = unreadText, - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - color = Color.White, - maxLines = 1 - ) - } - } } } } + } } /** - * 🔥 Компактный индикатор typing для списка чатов - * Голубой текст "typing" с анимированными точками + * 🔥 Компактный индикатор typing для списка чатов Голубой текст "typing" с анимированными точками */ @Composable fun TypingIndicatorSmall() { val infiniteTransition = rememberInfiniteTransition(label = "typing") val typingColor = PrimaryBlue - + Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(1.dp) + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(1.dp) ) { - Text( - text = "typing", - fontSize = 14.sp, - color = typingColor, - fontWeight = FontWeight.Medium - ) - + Text(text = "typing", fontSize = 14.sp, color = typingColor, fontWeight = FontWeight.Medium) + // 3 анимированные точки repeat(3) { index -> - val offsetY by infiniteTransition.animateFloat( - initialValue = 0f, - targetValue = -3f, - animationSpec = infiniteRepeatable( - animation = tween( - durationMillis = 500, - delayMillis = index * 120, - easing = FastOutSlowInEasing - ), - repeatMode = RepeatMode.Reverse - ), - label = "dot$index" - ) - + val offsetY by + infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = -3f, + animationSpec = + infiniteRepeatable( + animation = + tween( + durationMillis = 500, + delayMillis = index * 120, + easing = FastOutSlowInEasing + ), + repeatMode = RepeatMode.Reverse + ), + label = "dot$index" + ) + Text( - text = ".", - fontSize = 14.sp, - color = typingColor, - fontWeight = FontWeight.Medium, - modifier = Modifier.offset(y = offsetY.dp) + text = ".", + fontSize = 14.sp, + color = typingColor, + fontWeight = FontWeight.Medium, + modifier = Modifier.offset(y = offsetY.dp) ) } } } -/** - * 📬 Секция Requests - кнопка для перехода к списку запросов - */ +/** 📬 Секция Requests - кнопка для перехода к списку запросов */ @Composable -fun RequestsSection( - count: Int, - isDarkTheme: Boolean, - onClick: () -> Unit -) { +fun RequestsSection(count: Int, isDarkTheme: Boolean, onClick: () -> Unit) { val textColor = if (isDarkTheme) Color(0xFF4DABF7) else Color(0xFF228BE6) val arrowColor = if (isDarkTheme) Color(0xFFC9C9C9) else Color(0xFF228BE6) - + Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 14.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + modifier = + Modifier.fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Requests +$count", - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - color = textColor + text = "Requests +$count", + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = textColor ) - + Icon( - imageVector = Icons.Default.ChevronRight, - contentDescription = "Open requests", - tint = arrowColor, - modifier = Modifier.size(24.dp) + imageVector = Icons.Default.ChevronRight, + contentDescription = "Open requests", + tint = arrowColor.copy(alpha = 0.6f), + modifier = Modifier.size(24.dp) ) } } -/** - * 📬 Экран со списком Requests (без хедера - хедер в основном TopAppBar) - */ +/** 📬 Экран со списком Requests (без хедера - хедер в основном TopAppBar) */ @Composable fun RequestsScreen( - requests: List, - isDarkTheme: Boolean, - onBack: () -> Unit, - onRequestClick: (DialogUiModel) -> Unit + requests: List, + isDarkTheme: Boolean, + onBack: () -> Unit, + onRequestClick: (DialogUiModel) -> Unit ) { val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF) val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8) - - Column( - modifier = Modifier - .fillMaxSize() - .background(backgroundColor) - ) { + + Column(modifier = Modifier.fillMaxSize().background(backgroundColor)) { if (requests.isEmpty()) { // Empty state Box( - modifier = Modifier - .fillMaxSize() - .padding(32.dp), - contentAlignment = Alignment.Center + modifier = Modifier.fillMaxSize().padding(32.dp), + contentAlignment = Alignment.Center ) { Text( - text = "No requests", - fontSize = 16.sp, - color = if (isDarkTheme) Color(0xFF8E8E8E) else Color(0xFF8E8E93), - textAlign = TextAlign.Center + text = "No requests", + fontSize = 16.sp, + color = if (isDarkTheme) Color(0xFF8E8E8E) else Color(0xFF8E8E93), + textAlign = TextAlign.Center ) } } else { // Requests list - LazyColumn( - modifier = Modifier.fillMaxSize() - ) { + LazyColumn(modifier = Modifier.fillMaxSize()) { items(requests, key = { it.opponentKey }) { request -> DialogItemContent( - dialog = request, - isDarkTheme = isDarkTheme, - isTyping = false, - onClick = { onRequestClick(request) } + dialog = request, + isDarkTheme = isDarkTheme, + isTyping = false, + onClick = { onRequestClick(request) } ) - + Divider( - modifier = Modifier.padding(start = 84.dp), - color = dividerColor, - thickness = 0.5.dp + modifier = Modifier.padding(start = 84.dp), + color = dividerColor, + thickness = 0.5.dp ) } } @@ -1780,72 +1844,68 @@ fun RequestsScreen( } } -/** - * 🎨 Enhanced Drawer Menu Item - красивый пункт меню с hover эффектом - */ +/** 🎨 Enhanced Drawer Menu Item - красивый пункт меню с hover эффектом */ @Composable fun DrawerMenuItemEnhanced( - icon: androidx.compose.ui.graphics.vector.ImageVector, - text: String, - iconColor: Color, - textColor: Color, - badge: String? = null, - onClick: () -> Unit + icon: androidx.compose.ui.graphics.vector.ImageVector, + text: String, + iconColor: Color, + textColor: Color, + badge: String? = null, + onClick: () -> Unit ) { Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 20.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically + modifier = + Modifier.fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 20.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = icon, - contentDescription = null, - tint = iconColor, - modifier = Modifier.size(24.dp) + imageVector = icon, + contentDescription = null, + tint = iconColor, + modifier = Modifier.size(24.dp) ) - + Spacer(modifier = Modifier.width(20.dp)) - + Text( - text = text, - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - color = textColor, - modifier = Modifier.weight(1f) + text = text, + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = textColor, + modifier = Modifier.weight(1f) ) - + badge?.let { Box( - modifier = Modifier - .background( - color = Color(0xFF4A90D9), - shape = RoundedCornerShape(10.dp) - ) - .padding(horizontal = 8.dp, vertical = 2.dp) + modifier = + Modifier.background( + color = Color(0xFF4A90D9), + shape = RoundedCornerShape(10.dp) + ) + .padding(horizontal = 8.dp, vertical = 2.dp) ) { Text( - text = it, - fontSize = 12.sp, - fontWeight = FontWeight.Medium, - color = Color.White + text = it, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = Color.White ) } } } } -/** - * 📏 Drawer Divider - разделитель между секциями - */ +/** 📏 Drawer Divider - разделитель между секциями */ @Composable fun DrawerDivider(isDarkTheme: Boolean) { Spacer(modifier = Modifier.height(8.dp)) Divider( - modifier = Modifier.padding(horizontal = 20.dp), - color = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFEEEEEE), - thickness = 0.5.dp + modifier = Modifier.padding(horizontal = 20.dp), + color = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFEEEEEE), + thickness = 0.5.dp ) Spacer(modifier = Modifier.height(8.dp)) } 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 410bb3a..4e92797 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 @@ -136,7 +136,7 @@ fun ForwardChatPickerBottomSheet( Icon( Icons.Default.Close, contentDescription = "Close", - tint = secondaryTextColor + tint = secondaryTextColor.copy(alpha = 0.6f) ) } } 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 1a7696d..c0cba5f 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 @@ -126,7 +126,7 @@ fun SearchScreen( Icon( Icons.Default.ArrowBack, contentDescription = "Back", - tint = textColor + tint = textColor.copy(alpha = 0.6f) ) } @@ -189,7 +189,7 @@ fun SearchScreen( Icon( Icons.Default.Clear, contentDescription = "Clear", - tint = secondaryTextColor + tint = secondaryTextColor.copy(alpha = 0.6f) ) } } @@ -381,7 +381,7 @@ private fun RecentUserItem( Icon( Icons.Default.Close, contentDescription = "Remove", - tint = secondaryTextColor, + tint = secondaryTextColor.copy(alpha = 0.6f), modifier = Modifier.size(20.dp) ) } diff --git a/RECENT_UPDATES.md b/docs/RECENT_UPDATES.md similarity index 100% rename from RECENT_UPDATES.md rename to docs/RECENT_UPDATES.md diff --git a/SMOOTH_KEYBOARD_TRANSITION_PLAN.md b/docs/SMOOTH_KEYBOARD_TRANSITION_PLAN.md similarity index 100% rename from SMOOTH_KEYBOARD_TRANSITION_PLAN.md rename to docs/SMOOTH_KEYBOARD_TRANSITION_PLAN.md diff --git a/TESTING_GUIDE.md b/docs/TESTING_GUIDE.md similarity index 100% rename from TESTING_GUIDE.md rename to docs/TESTING_GUIDE.md