diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 1be6feb..1b9a08c 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -201,7 +201,7 @@ fun MainScreen( onToggleTheme: () -> Unit = {}, onLogout: () -> Unit = {} ) { - val accountName = account?.name ?: "Rosetta User" + val accountName = account?.publicKey ?: "04c266b98ae5" val accountPhone = account?.publicKey?.take(16)?.let { "+${it.take(1)} ${it.substring(1, 4)} ${it.substring(4, 7)}${it.substring(7)}" } ?: "+7 775 9932587" diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/AuthFlow.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/AuthFlow.kt index 2bdb59b..31ff094 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/AuthFlow.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/AuthFlow.kt @@ -130,9 +130,9 @@ fun AuthFlow( selectedAccountId = selectedAccountId, onUnlocked = { account -> onAuthComplete(account) }, onSwitchAccount = { - if (accounts.size > 1) { - currentScreen = AuthScreen.SELECT_ACCOUNT - } + // Navigate to create new account screen + currentScreen = AuthScreen.WELCOME + selectedAccountId = null } ) } 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 cf9962b..dc76e12 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 @@ -58,6 +58,24 @@ fun SetPasswordScreen( var error by remember { mutableStateOf(null) } var visible by remember { mutableStateOf(false) } + // Track keyboard visibility + val view = androidx.compose.ui.platform.LocalView.current + var isKeyboardVisible by remember { mutableStateOf(false) } + + DisposableEffect(view) { + val listener = android.view.ViewTreeObserver.OnGlobalLayoutListener { + val rect = android.graphics.Rect() + view.getWindowVisibleDisplayFrame(rect) + val screenHeight = view.rootView.height + val keypadHeight = screenHeight - rect.bottom + isKeyboardVisible = keypadHeight > screenHeight * 0.15 + } + view.viewTreeObserver.addOnGlobalLayoutListener(listener) + onDispose { + view.viewTreeObserver.removeOnGlobalLayoutListener(listener) + } + } + LaunchedEffect(Unit) { visible = true } @@ -109,9 +127,18 @@ fun SetPasswordScreen( .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(if (isKeyboardVisible) 8.dp else 16.dp)) + + // Lock Icon - smaller when keyboard is visible + val iconSize by animateDpAsState( + targetValue = if (isKeyboardVisible) 48.dp else 80.dp, + animationSpec = tween(300, easing = FastOutSlowInEasing) + ) + val iconInnerSize by animateDpAsState( + targetValue = if (isKeyboardVisible) 24.dp else 40.dp, + animationSpec = tween(300, easing = FastOutSlowInEasing) + ) - // Lock Icon AnimatedVisibility( visible = visible, enter = fadeIn(tween(500)) + scaleIn( @@ -121,8 +148,8 @@ fun SetPasswordScreen( ) { Box( modifier = Modifier - .size(80.dp) - .clip(RoundedCornerShape(20.dp)) + .size(iconSize) + .clip(RoundedCornerShape(if (isKeyboardVisible) 12.dp else 20.dp)) .background(PrimaryBlue.copy(alpha = 0.1f)), contentAlignment = Alignment.Center ) { @@ -130,12 +157,12 @@ fun SetPasswordScreen( Icons.Default.Lock, contentDescription = null, tint = PrimaryBlue, - modifier = Modifier.size(40.dp) + modifier = Modifier.size(iconInnerSize) ) } } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(if (isKeyboardVisible) 12.dp else 24.dp)) AnimatedVisibility( visible = visible, @@ -146,13 +173,13 @@ fun SetPasswordScreen( ) { Text( text = "Protect Your Account", - fontSize = 24.sp, + fontSize = if (isKeyboardVisible) 20.sp else 24.sp, fontWeight = FontWeight.Bold, color = textColor ) } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(if (isKeyboardVisible) 6.dp else 8.dp)) AnimatedVisibility( visible = visible, @@ -160,14 +187,14 @@ fun SetPasswordScreen( ) { Text( text = "This password encrypts your keys locally.\nYou'll need it to unlock Rosetta.", - fontSize = 14.sp, + fontSize = if (isKeyboardVisible) 12.sp else 14.sp, color = secondaryTextColor, textAlign = TextAlign.Center, - lineHeight = 20.sp + lineHeight = if (isKeyboardVisible) 16.sp else 20.sp ) } - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(if (isKeyboardVisible) 16.dp else 32.dp)) // Password Field AnimatedVisibility( 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 3149096..e00e492 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 @@ -5,6 +5,8 @@ import androidx.compose.animation.* import androidx.compose.animation.core.* import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions @@ -15,7 +17,11 @@ 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.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.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight @@ -24,18 +30,28 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation 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.sp import com.rosetta.messenger.R import com.rosetta.messenger.crypto.CryptoManager import com.rosetta.messenger.data.AccountManager import com.rosetta.messenger.data.DecryptedAccount +import com.rosetta.messenger.data.EncryptedAccount import com.rosetta.messenger.network.ProtocolManager -import com.rosetta.messenger.network.ProtocolState import com.rosetta.messenger.ui.onboarding.PrimaryBlue +import com.rosetta.messenger.ui.chats.getAvatarColor +import com.rosetta.messenger.ui.chats.getAvatarText import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +// Account model for dropdown +data class AccountItem( + val publicKey: String, + val name: String, + val encryptedAccount: EncryptedAccount +) + @OptIn(ExperimentalMaterial3Api::class) @Composable fun UnlockScreen( @@ -48,6 +64,7 @@ fun UnlockScreen( val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec) val textColor by animateColorAsState(if (isDarkTheme) Color.White else Color.Black, animationSpec = themeAnimSpec) val secondaryTextColor by animateColorAsState(if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), animationSpec = themeAnimSpec) + val cardBackground by animateColorAsState(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5), animationSpec = themeAnimSpec) val context = LocalContext.current val accountManager = remember { AccountManager(context) } @@ -57,17 +74,63 @@ fun UnlockScreen( var passwordVisible by remember { mutableStateOf(false) } var isUnlocking by remember { mutableStateOf(false) } var error by remember { mutableStateOf(null) } - var currentPublicKey by remember { mutableStateOf(null) } - // Load current account + // Account selection state + var accounts by remember { mutableStateOf>(emptyList()) } + var selectedAccount by remember { mutableStateOf(null) } + var isDropdownExpanded by remember { mutableStateOf(false) } + var searchQuery by remember { mutableStateOf("") } + val searchFocusRequester = remember { FocusRequester() } + + // Load accounts LaunchedEffect(Unit) { - currentPublicKey = selectedAccountId ?: accountManager.currentPublicKey.first() + val allAccounts = accountManager.getAllAccounts() + accounts = allAccounts.map { acc -> + AccountItem( + publicKey = acc.publicKey, + name = acc.name, + encryptedAccount = acc + ) + } + + // Select account + val targetPublicKey = selectedAccountId ?: accountManager.currentPublicKey.first() + selectedAccount = accounts.find { it.publicKey == targetPublicKey } ?: accounts.firstOrNull() + } + + // Filter accounts by search + val filteredAccounts = remember(searchQuery, accounts) { + if (searchQuery.isEmpty()) accounts + else accounts.filter { + it.name.contains(searchQuery, ignoreCase = true) || + it.publicKey.contains(searchQuery, ignoreCase = true) + } } // Entry animation var visible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { visible = true } + // Dropdown animation + val dropdownProgress by animateFloatAsState( + targetValue = if (isDropdownExpanded) 1f else 0f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow + ), + label = "dropdownProgress" + ) + + // Auto-focus search when dropdown opens + LaunchedEffect(isDropdownExpanded) { + if (isDropdownExpanded) { + kotlinx.coroutines.delay(200) + searchFocusRequester.requestFocus() + } else { + searchQuery = "" + } + } + Box( modifier = Modifier .fillMaxSize() @@ -76,12 +139,13 @@ fun UnlockScreen( Column( modifier = Modifier .fillMaxSize() + .verticalScroll(rememberScrollState()) .imePadding() .padding(horizontal = 24.dp) .statusBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.weight(0.2f)) + Spacer(modifier = Modifier.height(40.dp)) // Rosetta Logo AnimatedVisibility( @@ -92,12 +156,12 @@ fun UnlockScreen( painter = painterResource(id = R.drawable.rosetta_icon), contentDescription = "Rosetta", modifier = Modifier - .size(120.dp) + .size(100.dp) .clip(CircleShape) ) } - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(24.dp)) // Title AnimatedVisibility( @@ -117,24 +181,263 @@ fun UnlockScreen( Spacer(modifier = Modifier.height(8.dp)) - // Account info AnimatedVisibility( - visible = visible && currentPublicKey != null, + visible = visible, enter = fadeIn(tween(600, delayMillis = 300)) ) { Text( - text = "Enter your password to unlock", + text = "Select your account and enter password", fontSize = 16.sp, color = secondaryTextColor, textAlign = TextAlign.Center ) } - Spacer(modifier = Modifier.height(48.dp)) + Spacer(modifier = Modifier.height(32.dp)) + + // Account Selector Card + AnimatedVisibility( + visible = visible, + enter = fadeIn(tween(600, delayMillis = 350)) + slideInVertically( + initialOffsetY = { 50 }, + animationSpec = tween(600, delayMillis = 350) + ) + ) { + Column { + // Account selector dropdown + Card( + modifier = Modifier + .fillMaxWidth() + .clickable { isDropdownExpanded = !isDropdownExpanded }, + colors = CardDefaults.cardColors(containerColor = cardBackground), + shape = RoundedCornerShape(16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Avatar + if (selectedAccount != null) { + val avatarColors = getAvatarColor(selectedAccount!!.name, isDarkTheme) + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(avatarColors.backgroundColor), + contentAlignment = Alignment.Center + ) { + Text( + text = getAvatarText(selectedAccount!!.publicKey), + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = avatarColors.textColor + ) + } + } + + Spacer(modifier = Modifier.width(12.dp)) + + // Account info + Column(modifier = Modifier.weight(1f)) { + Text( + text = selectedAccount?.name ?: "Select Account", + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = textColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (selectedAccount != null) { + Text( + text = selectedAccount!!.publicKey.take(20) + "...", + fontSize = 13.sp, + color = secondaryTextColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + // Dropdown arrow with rotation + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + tint = secondaryTextColor, + modifier = Modifier + .size(24.dp) + .graphicsLayer { + rotationZ = 180f * dropdownProgress + } + ) + } + } + + // Dropdown list with animation + AnimatedVisibility( + visible = isDropdownExpanded, + enter = fadeIn(tween(200)) + expandVertically( + expandFrom = Alignment.Top, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMediumLow + ) + ), + exit = fadeOut(tween(150)) + shrinkVertically( + shrinkTowards = Alignment.Top, + animationSpec = tween(200) + ) + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .heightIn(max = 300.dp), + colors = CardDefaults.cardColors(containerColor = cardBackground), + shape = RoundedCornerShape(16.dp) + ) { + Column { + // Search field + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + placeholder = { + Text( + "Search accounts...", + color = secondaryTextColor.copy(alpha = 0.6f) + ) + }, + leadingIcon = { + Icon( + Icons.Default.Search, + contentDescription = null, + tint = secondaryTextColor + ) + }, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = PrimaryBlue, + unfocusedBorderColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + focusedTextColor = textColor, + unfocusedTextColor = textColor, + cursorColor = PrimaryBlue + ), + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp) + .focusRequester(searchFocusRequester), + shape = RoundedCornerShape(12.dp) + ) + + Divider( + color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0), + thickness = 0.5.dp + ) + + // Account list + LazyColumn( + modifier = Modifier.fillMaxWidth() + ) { + items(filteredAccounts, key = { it.publicKey }) { account -> + val isSelected = account.publicKey == selectedAccount?.publicKey + val itemScale by animateFloatAsState( + targetValue = if (isSelected) 1f else 0.98f, + label = "itemScale" + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .scale(itemScale) + .clickable { + selectedAccount = account + isDropdownExpanded = false + password = "" + error = null + } + .background( + if (isSelected) PrimaryBlue.copy(alpha = 0.1f) + else Color.Transparent + ) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Avatar + val avatarColors = getAvatarColor(account.name, isDarkTheme) + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(avatarColors.backgroundColor), + contentAlignment = Alignment.Center + ) { + Text( + text = getAvatarText(account.publicKey), + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = avatarColors.textColor + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = account.name, + fontSize = 15.sp, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, + color = textColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = account.publicKey.take(16) + "...", + fontSize = 12.sp, + color = secondaryTextColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + if (isSelected) { + Icon( + Icons.Default.Check, + contentDescription = null, + tint = PrimaryBlue, + modifier = Modifier.size(20.dp) + ) + } + } + } + + if (filteredAccounts.isEmpty()) { + item { + Text( + text = "No accounts found", + color = secondaryTextColor, + fontSize = 14.sp, + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + textAlign = TextAlign.Center + ) + } + } + } + } + } + } + } + } + + Spacer(modifier = Modifier.height(20.dp)) // Password Field AnimatedVisibility( - visible = visible, + visible = visible && !isDropdownExpanded, enter = fadeIn(tween(600, delayMillis = 400)) + slideInVertically( initialOffsetY = { 50 }, animationSpec = tween(600, delayMillis = 400) @@ -197,7 +500,7 @@ fun UnlockScreen( // Unlock Button AnimatedVisibility( - visible = visible, + visible = visible && !isDropdownExpanded, enter = fadeIn(tween(600, delayMillis = 500)) + slideInVertically( initialOffsetY = { 50 }, animationSpec = tween(600, delayMillis = 500) @@ -205,6 +508,10 @@ fun UnlockScreen( ) { Button( onClick = { + if (selectedAccount == null) { + error = "Please select an account" + return@Button + } if (password.isEmpty()) { error = "Please enter your password" return@Button @@ -213,20 +520,7 @@ fun UnlockScreen( isUnlocking = true scope.launch { try { - val publicKey = currentPublicKey ?: run { - error = "No account found" - isUnlocking = false - return@launch - } - - val accounts = accountManager.getAllAccounts() - val account = accounts.find { it.publicKey == publicKey } - - if (account == null) { - error = "Account not found" - isUnlocking = false - return@launch - } + val account = selectedAccount!!.encryptedAccount // Try to decrypt val decryptedPrivateKey = CryptoManager.decryptWithPassword( @@ -253,20 +547,20 @@ fun UnlockScreen( name = account.name ) - // 🔌 Connect to server and authenticate - Log.d("UnlockScreen", "🔌 Connecting to server...") + // Connect to server and authenticate + Log.d("UnlockScreen", "Connecting to server...") ProtocolManager.authenticate(account.publicKey, privateKeyHash) - accountManager.setCurrentAccount(publicKey) + accountManager.setCurrentAccount(account.publicKey) onUnlocked(decryptedAccount) } catch (e: Exception) { - error = "Failed to unlock: ${e.message}" + error = "Failed to unlock: \${e.message}" isUnlocking = false } } }, - enabled = password.isNotEmpty() && !isUnlocking, + enabled = selectedAccount != null && password.isNotEmpty() && !isUnlocking, modifier = Modifier .fillMaxWidth() .height(56.dp), @@ -302,9 +596,9 @@ fun UnlockScreen( Spacer(modifier = Modifier.height(16.dp)) - // Switch Account button + // Create New Account button AnimatedVisibility( - visible = visible, + visible = visible && !isDropdownExpanded, enter = fadeIn(tween(600, delayMillis = 600)) + slideInVertically( initialOffsetY = { 50 }, animationSpec = tween(600, delayMillis = 600) @@ -314,21 +608,21 @@ fun UnlockScreen( onClick = onSwitchAccount ) { Icon( - imageVector = Icons.Default.SwapHoriz, + imageVector = Icons.Default.PersonAdd, contentDescription = null, tint = PrimaryBlue, modifier = Modifier.size(20.dp) ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Switch Account", + text = "Create New Account", color = PrimaryBlue, fontSize = 15.sp ) } } - Spacer(modifier = Modifier.weight(0.3f)) + Spacer(modifier = Modifier.height(60.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 47c0343..1f57a06 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 @@ -148,73 +148,30 @@ fun ChatsListScreen( onNewChat: () -> Unit, onLogout: () -> Unit ) { - // Theme transition animation state - var isTransitioning by remember { mutableStateOf(false) } - var transitionProgress by remember { mutableStateOf(0f) } - var clickPosition by remember { mutableStateOf(Offset.Zero) } - var shouldUpdateStatusBar by remember { mutableStateOf(false) } + // Theme transition state var hasInitialized by remember { mutableStateOf(false) } - var previousTheme by remember { mutableStateOf(isDarkTheme) } - var targetTheme by remember { mutableStateOf(isDarkTheme) } LaunchedEffect(Unit) { hasInitialized = true } - // Theme transition animation - LaunchedEffect(isTransitioning) { - if (isTransitioning) { - shouldUpdateStatusBar = false - val duration = 800f - val startTime = System.currentTimeMillis() - while (transitionProgress < 1f) { - val elapsed = System.currentTimeMillis() - startTime - transitionProgress = (elapsed / duration).coerceAtMost(1f) - kotlinx.coroutines.delay(16) - } - shouldUpdateStatusBar = true - kotlinx.coroutines.delay(50) - isTransitioning = false - transitionProgress = 0f - shouldUpdateStatusBar = false - previousTheme = targetTheme - } - } - val view = androidx.compose.ui.platform.LocalView.current - // Animate navigation bar color starting at 80% of wave animation - LaunchedEffect(isTransitioning, transitionProgress) { - if (isTransitioning && transitionProgress >= 0.8f && !view.isInEditMode) { - val window = (view.context as android.app.Activity).window - val navProgress = ((transitionProgress - 0.8f) / 0.2f).coerceIn(0f, 1f) - - val oldColor = if (previousTheme) 0xFF1A1A1A else 0xFFFFFFFF - val newColor = if (targetTheme) 0xFF1A1A1A else 0xFFFFFFFF - - val r1 = (oldColor shr 16 and 0xFF) - val g1 = (oldColor shr 8 and 0xFF) - val b1 = (oldColor and 0xFF) - val r2 = (newColor shr 16 and 0xFF) - val g2 = (newColor shr 8 and 0xFF) - val b2 = (newColor and 0xFF) - - val r = (r1 + (r2 - r1) * navProgress).toInt() - val g = (g1 + (g2 - g1) * navProgress).toInt() - val b = (b1 + (b2 - b1) * navProgress).toInt() - - window.navigationBarColor = (0xFF000000 or (r.toLong() shl 16) or (g.toLong() shl 8) or b.toLong()).toInt() - } - } - - // Update status bar icons when animation finishes - LaunchedEffect(shouldUpdateStatusBar) { - if (shouldUpdateStatusBar && !view.isInEditMode) { + // Update status bar and navigation bar + LaunchedEffect(isDarkTheme, drawerState.isOpen) { + if (!view.isInEditMode) { val window = (view.context as android.app.Activity).window val insetsController = androidx.core.view.WindowCompat.getInsetsController(window, view) insetsController.isAppearanceLightStatusBars = !isDarkTheme insetsController.isAppearanceLightNavigationBars = !isDarkTheme window.statusBarColor = android.graphics.Color.TRANSPARENT + // When drawer is open, dim the navigation bar to match overlay + window.navigationBarColor = if (drawerState.isOpen) { + // Darker color to match scrim overlay + if (isDarkTheme) 0xFF0D0D0D.toInt() else 0xFF999999.toInt() + } else { + if (isDarkTheme) 0xFF1A1A1A.toInt() else 0xFFFFFFFF.toInt() + } } } @@ -371,29 +328,12 @@ fun ChatsListScreen( ) Box(modifier = Modifier.fillMaxSize()) { - // Base background - shows the OLD theme color during transition + // Simple background Box( modifier = Modifier .fillMaxSize() - .background(if (isTransitioning) { - if (previousTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF) - } else backgroundColor) + .background(backgroundColor) ) - - // Circular reveal overlay - draws the NEW theme color expanding - if (isTransitioning) { - Canvas(modifier = Modifier.fillMaxSize()) { - val maxRadius = kotlin.math.hypot(size.width, size.height) - val radius = maxRadius * transitionProgress - - // Draw the NEW theme color expanding from click point - drawCircle( - color = if (targetTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF), - radius = radius, - center = clickPosition - ) - } - } ModalNavigationDrawer( drawerState = drawerState, @@ -406,8 +346,9 @@ fun ChatsListScreen( Box( modifier = Modifier .fillMaxWidth() - .background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)) - .padding(16.dp) + .background(drawerBackgroundColor) + .padding(top = 48.dp) + .padding(horizontal = 16.dp, vertical = 16.dp) ) { Column { Row( @@ -434,46 +375,25 @@ fun ChatsListScreen( // Theme toggle IconButton( - onClick = {}, - modifier = Modifier.onGloballyPositioned { coordinates -> - // This will be handled by clickable below - } + onClick = onToggleTheme ) { - Box( - modifier = Modifier - .clickable { - if (!isTransitioning) { - previousTheme = isDarkTheme - targetTheme = !isDarkTheme - // Use center of icon as click position - val screenWidth = view.width.toFloat() - val screenHeight = view.height.toFloat() - clickPosition = Offset( - screenWidth - 48.dp.value * view.resources.displayMetrics.density, - 96.dp.value * view.resources.displayMetrics.density - ) - isTransitioning = true - onToggleTheme() - } - } - ) { - Icon( - if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode, - contentDescription = "Toggle theme", - tint = textColor - ) - } + Icon( + if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode, + contentDescription = "Toggle theme", + tint = textColor + ) } } Spacer(modifier = Modifier.height(16.dp)) - // Account name + // Public key instead of account name Text( - text = accountName, - fontSize = 18.sp, - fontWeight = FontWeight.SemiBold, - color = textColor + text = accountPublicKey, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = secondaryTextColor, + maxLines = 1 ) } } @@ -508,6 +428,8 @@ fun ChatsListScreen( } } + Spacer(modifier = Modifier.weight(1f)) + // Logout button at bottom Divider( modifier = Modifier.padding(horizontal = 16.dp), @@ -545,6 +467,7 @@ fun ChatsListScreen( ) } + Spacer(modifier = Modifier.navigationBarsPadding()) Spacer(modifier = Modifier.height(16.dp)) } }