From e208ce050a73ea0dad1d383448bf7ff8b6c79376 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Thu, 12 Feb 2026 20:09:53 +0500 Subject: [PATCH] feat: simplify color handling in ChatsListScreen and improve gesture callbacks in SwipeableDialogItem --- app/src/main/AndroidManifest.xml | 1 + .../com/rosetta/messenger/MainActivity.kt | 74 +++++- .../rosetta/messenger/data/AccountManager.kt | 26 ++ .../messenger/database/BlacklistEntities.kt | 4 + .../messenger/database/MessageEntities.kt | 8 + .../messenger/ui/chats/ChatDetailScreen.kt | 12 +- .../messenger/ui/chats/ChatsListScreen.kt | 185 ++++---------- .../ui/chats/ForwardChatPickerBottomSheet.kt | 10 +- .../messenger/ui/chats/SearchScreen.kt | 11 +- .../chats/components/ChatDetailComponents.kt | 18 +- .../ui/components/BlurredAvatarBackground.kt | 117 ++++----- .../messenger/ui/settings/AppearanceColors.kt | 2 +- .../messenger/ui/settings/AppearanceScreen.kt | 16 +- .../ui/settings/OtherProfileScreen.kt | 28 ++- .../messenger/ui/settings/ProfileScreen.kt | 229 +++++++++++------- .../messenger/ui/settings/SafetyScreen.kt | 43 +++- 16 files changed, 419 insertions(+), 365 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2864faa..2b17ebb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ + val shortKey = acc.publicKey.take(7) + val displayName = acc.name ?: shortKey + val initials = displayName.trim() + .split(Regex("\\s+")) + .filter { it.isNotEmpty() } + .let { words -> + when { + words.isEmpty() -> "??" + words.size == 1 -> words[0].take(2).uppercase() + else -> "${words[0].first()}${words[1].first()}".uppercase() + } + } + AccountInfo( + id = acc.publicKey, + name = displayName, + username = acc.username ?: "", + initials = initials, + publicKey = acc.publicKey + ) + } + hasExistingAccount = accounts.isNotEmpty() + // 8. Navigate away last + currentAccount = null + } catch (e: Exception) { + android.util.Log.e("DeleteAccount", "Failed to delete account", e) + } + } + }, onAccountInfoUpdated = { // Reload account list when profile is updated val accounts = accountManager.getAllAccounts() @@ -509,6 +559,7 @@ fun MainScreen( onToggleTheme: () -> Unit = {}, onThemeModeChange: (String) -> Unit = {}, onLogout: () -> Unit = {}, + onDeleteAccount: () -> Unit = {}, onAccountInfoUpdated: suspend () -> Unit = {} ) { // Reactive state for account name and username @@ -758,7 +809,6 @@ fun MainScreen( onNavigateToAppearance = { pushScreen(Screen.Appearance) }, onNavigateToSafety = { pushScreen(Screen.Safety) }, onNavigateToLogs = { pushScreen(Screen.Logs) }, - onNavigateToCrashLogs = { pushScreen(Screen.CrashLogs) }, onNavigateToBiometric = { pushScreen(Screen.Biometric) }, viewModel = profileViewModel, avatarRepository = avatarRepository, @@ -770,13 +820,13 @@ fun MainScreen( // Other screens with swipe back SwipeBackContainer( isVisible = isBackupVisible, - onBack = { navStack = navStack.filterNot { it is Screen.Backup } + Screen.Safety }, + onBack = { navStack = navStack.filterNot { it is Screen.Backup } }, isDarkTheme = isDarkTheme ) { BackupScreen( isDarkTheme = isDarkTheme, onBack = { - navStack = navStack.filterNot { it is Screen.Backup } + Screen.Safety + navStack = navStack.filterNot { it is Screen.Backup } }, onVerifyPassword = { password -> // Verify password by trying to decrypt the private key @@ -824,11 +874,9 @@ fun MainScreen( accountPrivateKey = accountPrivateKey, onBack = { navStack = navStack.filterNot { it is Screen.Safety } }, onBackupClick = { - navStack = navStack.filterNot { it is Screen.Safety } + Screen.Backup + navStack = navStack + Screen.Backup }, - onDeleteAccount = { - // TODO: Implement account deletion - } + onDeleteAccount = onDeleteAccount ) } @@ -893,8 +941,13 @@ fun MainScreen( currentUserName = accountName, onBack = { popChatAndChildren() }, onUserProfileClick = { user -> - // Открываем профиль другого пользователя - pushScreen(Screen.OtherProfile(user)) + if (user.publicKey == accountPublicKey) { + // Свой профиль — открываем My Profile + pushScreen(Screen.Profile) + } else { + // Открываем профиль другого пользователя + pushScreen(Screen.OtherProfile(user)) + } }, onNavigateToChat = { forwardUser -> // 📨 Forward: переход в выбранный чат с полными данными @@ -926,6 +979,9 @@ fun MainScreen( navStack = navStack.filterNot { it is Screen.Search } + Screen.ChatDetail(selectedSearchUser) + }, + onNavigateToCrashLogs = { + navStack = navStack.filterNot { it is Screen.Search } + Screen.CrashLogs } ) } diff --git a/app/src/main/java/com/rosetta/messenger/data/AccountManager.kt b/app/src/main/java/com/rosetta/messenger/data/AccountManager.kt index 53a08ec..1c95dbb 100644 --- a/app/src/main/java/com/rosetta/messenger/data/AccountManager.kt +++ b/app/src/main/java/com/rosetta/messenger/data/AccountManager.kt @@ -118,6 +118,32 @@ class AccountManager(private val context: Context) { saveAccount(updatedAccount) } + /** + * Delete account completely - remove from accounts list and clear login state + */ + suspend fun deleteAccount(publicKey: String) { + context.accountDataStore.edit { preferences -> + // Remove from accounts list + val existingJson = preferences[ACCOUNTS_JSON] + if (existingJson != null) { + val accounts = parseAccounts(existingJson).toMutableList() + accounts.removeAll { it.publicKey == publicKey } + preferences[ACCOUNTS_JSON] = serializeAccounts(accounts) + } + // Clear current login if this was the active account + val currentKey = preferences[CURRENT_PUBLIC_KEY] + if (currentKey == publicKey) { + preferences[IS_LOGGED_IN] = false + preferences.remove(CURRENT_PUBLIC_KEY) + } + } + // Clear SharedPreferences if this was the last logged account + val lastLogged = sharedPrefs.getString(KEY_LAST_LOGGED, null) + if (lastLogged == publicKey) { + sharedPrefs.edit().remove(KEY_LAST_LOGGED).commit() + } + } + suspend fun clearAll() { context.accountDataStore.edit { it.clear() } } diff --git a/app/src/main/java/com/rosetta/messenger/database/BlacklistEntities.kt b/app/src/main/java/com/rosetta/messenger/database/BlacklistEntities.kt index 75b58d9..65cff36 100644 --- a/app/src/main/java/com/rosetta/messenger/database/BlacklistEntities.kt +++ b/app/src/main/java/com/rosetta/messenger/database/BlacklistEntities.kt @@ -64,4 +64,8 @@ interface BlacklistDao { */ @Query("SELECT public_key FROM blacklist WHERE account = :account") suspend fun getBlockedPublicKeys(account: String): List + + /** Удалить все записи blacklist для аккаунта */ + @Query("DELETE FROM blacklist WHERE account = :account") + suspend fun deleteAllByAccount(account: String) } diff --git a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt index 8579498..622c0cb 100644 --- a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt +++ b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt @@ -277,6 +277,10 @@ interface MessageDao { ) suspend fun deleteMessagesBetweenUsers(account: String, user1: String, user2: String): Int + /** Удалить все сообщения аккаунта */ + @Query("DELETE FROM messages WHERE account = :account") + suspend fun deleteAllByAccount(account: String): Int + /** Количество непрочитанных сообщений в диалоге */ @Query( """ @@ -492,6 +496,10 @@ interface DialogDao { @Query("DELETE FROM dialogs WHERE account = :account AND opponent_key = :opponentKey") suspend fun deleteDialog(account: String, opponentKey: String) + /** Удалить все диалоги аккаунта */ + @Query("DELETE FROM dialogs WHERE account = :account") + suspend fun deleteAllByAccount(account: String) + /** Обновить информацию о собеседнике */ @Query( """ 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 a3f5dfd..9367aa3 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 @@ -2267,17 +2267,7 @@ fun ChatDetailScreen( ) } - // 🐛 Debug Logs BottomSheet - if (showDebugLogs) { - DebugLogsBottomSheet( - logs = debugLogs, - isDarkTheme = isDarkTheme, - onDismiss = { showDebugLogs = false }, - onClearLogs = { ProtocolManager.clearLogs() } - ) - } - - // 📨 Forward Chat Picker BottomSheet + // Forward Chat Picker BottomSheet if (showForwardPicker) { ForwardChatPickerBottomSheet( dialogs = dialogsList, 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 fb0da86..beb5c8a 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 @@ -46,6 +46,7 @@ import com.rosetta.messenger.ui.components.AvatarImage import com.rosetta.messenger.ui.components.BlurredAvatarBackground import com.rosetta.messenger.ui.components.VerifiedBadge import com.rosetta.messenger.ui.onboarding.PrimaryBlue +import com.rosetta.messenger.ui.onboarding.PrimaryBlueDark import com.rosetta.messenger.ui.settings.BackgroundBlurPresets import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback @@ -237,9 +238,6 @@ fun ChatsListScreen( val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black } val secondaryTextColor = remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E8E) else Color(0xFF8E8E93) } - val drawerGrabZonePx = with(androidx.compose.ui.platform.LocalDensity.current) { 88.dp.toPx() } - val drawerOpenDistancePx = with(androidx.compose.ui.platform.LocalDensity.current) { 20.dp.toPx() } - val drawerOpenVelocityThresholdPx = with(androidx.compose.ui.platform.LocalDensity.current) { 110.dp.toPx() } // Protocol connection state val protocolState by ProtocolManager.state.collectAsState() @@ -277,7 +275,7 @@ fun ChatsListScreen( var visible by rememberSaveable { mutableStateOf(true) } // Confirmation dialogs state - var dialogToDelete by remember { mutableStateOf(null) } + var dialogsToDelete by remember { mutableStateOf>(emptyList()) } var dialogToBlock by remember { mutableStateOf(null) } var dialogToUnblock by remember { mutableStateOf(null) } @@ -290,12 +288,16 @@ fun ChatsListScreen( val mutedChats by preferencesManager.mutedChatsForAccount(accountPublicKey) .collectAsState(initial = emptySet()) - // Перехватываем системный back gesture - не закрываем приложение + val activity = context as? android.app.Activity + + // Всегда перехватываем back чтобы predictive back анимация не ломала UI BackHandler(enabled = true) { if (isSelectionMode) { selectedChatKeys = emptySet() } else if (drawerState.isOpen) { scope.launch { drawerState.close() } + } else { + activity?.moveTaskToBack(true) } } @@ -447,96 +449,10 @@ fun ChatsListScreen( Modifier.fillMaxSize() .background(backgroundColor) .navigationBarsPadding() - .pointerInput(drawerState.isOpen, showRequestsScreen) { - if (showRequestsScreen) return@pointerInput - - val velocityTracker = VelocityTracker() - val relaxedTouchSlop = viewConfiguration.touchSlop * 0.8f - - awaitEachGesture { - val down = - awaitFirstDown(requireUnconsumed = false) - - if (drawerState.isOpen || down.position.x > drawerGrabZonePx) { - return@awaitEachGesture - } - - velocityTracker.resetTracking() - velocityTracker.addPosition( - down.uptimeMillis, - down.position - ) - var totalDragX = 0f - var totalDragY = 0f - var claimed = false - - while (true) { - val event = awaitPointerEvent() - val change = - event.changes.firstOrNull { - it.id == down.id - } - ?: break - - if (change.changedToUpIgnoreConsumed()) break - - val delta = change.positionChange() - totalDragX += delta.x - totalDragY += delta.y - velocityTracker.addPosition( - change.uptimeMillis, - change.position - ) - - if (!claimed) { - val distance = - kotlin.math.sqrt( - totalDragX * - totalDragX + - totalDragY * - totalDragY - ) - if (distance < relaxedTouchSlop) - continue - - val horizontalDominance = - kotlin.math.abs( - totalDragX - ) > - kotlin.math.abs( - totalDragY - ) * 1.15f - if ( - totalDragX > 0 && - horizontalDominance - ) { - claimed = true - change.consume() - } else { - break - } - } else { - change.consume() - } - } - - val velocityX = velocityTracker.calculateVelocity().x - val shouldOpenDrawer = - claimed && - (totalDragX >= - drawerOpenDistancePx || - velocityX > - drawerOpenVelocityThresholdPx) - - if (shouldOpenDrawer && drawerState.isClosed) { - scope.launch { drawerState.open() } - } - } - } ) { ModalNavigationDrawer( drawerState = drawerState, - gesturesEnabled = !showRequestsScreen, // Disable drawer swipe when requests are open + gesturesEnabled = !showRequestsScreen, drawerContent = { ModalDrawerSheet( drawerContainerColor = Color.Transparent, @@ -571,7 +487,8 @@ fun ChatsListScreen( BackgroundBlurPresets .getOverlayColors( backgroundBlurColorId - ) + ), + isDarkTheme = isDarkTheme ) // Content поверх фона @@ -634,11 +551,7 @@ fun ChatsListScreen( contentDescription = if (isDarkTheme) "Light Mode" else "Dark Mode", - tint = - if (isDarkTheme) - Color.White.copy(alpha = 0.8f) - else - Color.Black.copy(alpha = 0.7f), + tint = Color.White, modifier = Modifier.size(22.dp) ) } @@ -646,7 +559,7 @@ fun ChatsListScreen( Spacer( modifier = - Modifier.height(14.dp) + Modifier.height(8.dp) ) // Display name @@ -656,11 +569,7 @@ fun ChatsListScreen( fontSize = 16.sp, fontWeight = FontWeight.SemiBold, - color = - if (isDarkTheme) - Color.White - else - Color.Black + color = Color.White ) } @@ -674,13 +583,7 @@ fun ChatsListScreen( text = "@$accountUsername", fontSize = 13.sp, - color = - if (isDarkTheme) - Color.White - .copy(alpha = 0.7f) - else - Color.Black - .copy(alpha = 0.7f) + color = Color.White ) } } @@ -699,7 +602,10 @@ fun ChatsListScreen( .padding(vertical = 8.dp) ) { val menuIconColor = - textColor.copy(alpha = 0.6f) + if (isDarkTheme) Color(0xFF7A7F85) + else textColor.copy(alpha = 0.6f) + + val accentColor = if (isDarkTheme) PrimaryBlueDark else PrimaryBlue // 👤 Profile DrawerMenuItemEnhanced( @@ -826,6 +732,7 @@ fun ChatsListScreen( } } ) { + Box(modifier = Modifier.fillMaxSize()) { Scaffold( topBar = { key(isDarkTheme, showRequestsScreen, isSelectionMode) { @@ -876,9 +783,8 @@ fun ChatsListScreen( // Delete IconButton(onClick = { val allDialogs = topLevelChatsState.dialogs - val first = selectedChatKeys.firstOrNull() - val dlg = allDialogs.find { it.opponentKey == first } - if (dlg != null) dialogToDelete = dlg + val selected = allDialogs.filter { selectedChatKeys.contains(it.opponentKey) } + if (selected.isNotEmpty()) dialogsToDelete = selected selectedChatKeys = emptySet() }) { Icon( @@ -956,8 +862,8 @@ fun ChatsListScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4), - scrolledContainerColor = if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4), + containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4), + scrolledContainerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4), navigationIconContentColor = Color.White, titleContentColor = Color.White, actionIconContentColor = Color.White @@ -1089,9 +995,9 @@ fun ChatsListScreen( colors = TopAppBarDefaults.topAppBarColors( containerColor = - if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4), + if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4), scrolledContainerColor = - if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4), + if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4), navigationIconContentColor = Color.White, titleContentColor = @@ -1493,8 +1399,8 @@ fun ChatsListScreen( selectedChatKeys + dialog.opponentKey }, onDelete = { - dialogToDelete = - dialog + dialogsToDelete = + listOf(dialog) }, onBlock = { dialogToBlock = @@ -1539,44 +1445,49 @@ fun ChatsListScreen( // Console button removed } } + } // Close content Box } // Close ModalNavigationDrawer // 🔥 Confirmation Dialogs // Delete Dialog Confirmation - dialogToDelete?.let { dialog -> + if (dialogsToDelete.isNotEmpty()) { + val count = dialogsToDelete.size AlertDialog( - onDismissRequest = { dialogToDelete = null }, + onDismissRequest = { dialogsToDelete = emptyList() }, containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, title = { Text( - "Delete Chat", + if (count == 1) "Delete Chat" else "Delete $count Chats", fontWeight = FontWeight.Bold, color = textColor ) }, text = { Text( - "Are you sure you want to delete this chat? This action cannot be undone.", + if (count == 1) "Are you sure you want to delete this chat? This action cannot be undone." + else "Are you sure you want to delete $count chats? This action cannot be undone.", color = secondaryTextColor ) }, confirmButton = { TextButton( onClick = { - val opponentKey = dialog.opponentKey - dialogToDelete = null + val toDelete = dialogsToDelete.toList() + dialogsToDelete = emptyList() scope.launch { - chatsViewModel.deleteDialog( - opponentKey - ) + toDelete.forEach { dialog -> + chatsViewModel.deleteDialog( + dialog.opponentKey + ) + } } } ) { Text("Delete", color = Color(0xFFFF3B30)) } }, dismissButton = { - TextButton(onClick = { dialogToDelete = null }) { + TextButton(onClick = { dialogsToDelete = emptyList() }) { Text("Cancel", color = PrimaryBlue) } } @@ -1662,6 +1573,7 @@ fun ChatsListScreen( } ) } + } // Close Box } @@ -2272,6 +2184,9 @@ fun SwipeableDialogItem( } // 2. КОНТЕНТ - поверх кнопок, сдвигается при свайпе + // 🔥 rememberUpdatedState чтобы pointerInput всегда вызывал актуальные callbacks + val currentOnClick by rememberUpdatedState(onClick) + val currentOnLongClick by rememberUpdatedState(onLongClick) Column( modifier = Modifier.fillMaxSize() @@ -2338,12 +2253,12 @@ fun SwipeableDialogItem( when (gestureType) { "tap" -> { - onClick() + currentOnClick() return@awaitEachGesture } "cancelled" -> return@awaitEachGesture "longpress" -> { - onLongClick() + currentOnLongClick() // Consume remaining events until finger lifts while (true) { val event = awaitPointerEvent() @@ -3317,7 +3232,7 @@ fun DrawerMenuItemEnhanced( Text( text = text, fontSize = 16.sp, - fontWeight = FontWeight.Normal, + fontWeight = FontWeight.Medium, color = textColor, modifier = Modifier.weight(1f) ) @@ -3348,7 +3263,7 @@ 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), + color = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFC8C8CC), 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 b3b5b8b..14f2e31 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 @@ -225,9 +225,9 @@ fun ForwardChatPickerBottomSheet( onClick = { view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) selectedChats = if (isSelected) { - selectedChats - dialog.opponentKey + emptySet() } else { - selectedChats + dialog.opponentKey + setOf(dialog.opponentKey) } } ) @@ -263,6 +263,7 @@ fun ForwardChatPickerBottomSheet( shape = RoundedCornerShape(12.dp), colors = ButtonDefaults.buttonColors( containerColor = PrimaryBlue, + contentColor = Color.White, disabledContainerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFD1D1D6), disabledContentColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF999999) ) @@ -274,10 +275,7 @@ fun ForwardChatPickerBottomSheet( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = if (hasSelection) - "Forward to ${selectedChats.size} chat${if (selectedChats.size > 1) "s" else ""}" - else - "Select a chat", + text = if (hasSelection) "Forward" else "Select a chat", fontSize = 16.sp, fontWeight = FontWeight.SemiBold ) 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 efb93b1..c7ecc66 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 @@ -54,7 +54,8 @@ fun SearchScreen( isDarkTheme: Boolean, protocolState: ProtocolState, onBackClick: () -> Unit, - onUserSelect: (SearchUser) -> Unit + onUserSelect: (SearchUser) -> Unit, + onNavigateToCrashLogs: () -> Unit = {} ) { // Context и View для мгновенного закрытия клавиатуры val context = LocalContext.current @@ -84,6 +85,14 @@ fun SearchScreen( val searchResults by searchViewModel.searchResults.collectAsState() val isSearching by searchViewModel.isSearching.collectAsState() + // Easter egg: navigate to CrashLogs when typing "rosettadev1" + LaunchedEffect(searchQuery) { + if (searchQuery.trim().equals("rosettadev1", ignoreCase = true)) { + searchViewModel.clearSearchQuery() + onNavigateToCrashLogs() + } + } + // Always reset query/results when leaving Search screen (back/swipe/navigation). DisposableEffect(Unit) { onDispose { searchViewModel.clearSearchQuery() } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt index 78f05cd..d39d40c 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt @@ -1753,23 +1753,7 @@ fun KebabMenu( ) } - // Debug Logs - KebabMenuItem( - icon = TablerIcons.Bug, - text = "Debug Logs", - onClick = onLogsClick, - tintColor = PrimaryBlue, - textColor = if (isDarkTheme) Color.White else Color.Black - ) - - Box( - modifier = - Modifier.fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 4.dp) - .height(0.5.dp) - .background(dividerColor) - ) - + // Delete chat KebabMenuItem( icon = TablerIcons.Trash, text = "Delete Chat", diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/BlurredAvatarBackground.kt b/app/src/main/java/com/rosetta/messenger/ui/components/BlurredAvatarBackground.kt index 16511fa..c6bc14a 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/BlurredAvatarBackground.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/BlurredAvatarBackground.kt @@ -38,37 +38,63 @@ fun BoxScope.BlurredAvatarBackground( avatarRepository: AvatarRepository?, fallbackColor: Color, blurRadius: Float = 25f, - alpha: Float = 0.3f, - overlayColors: List? = null + alpha: Float = 0.9f, + overlayColors: List? = null, + isDarkTheme: Boolean = true ) { - // Получаем аватары из репозитория - val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState() + // В светлой теме: если дефолтный фон (avatar) — синий как шапка chat list, + // если выбран кастомный цвет в Appearance — используем его + if (!isDarkTheme) { + val lightBgModifier = if (overlayColors != null && overlayColors.isNotEmpty()) { + if (overlayColors.size == 1) { + Modifier.matchParentSize().background(overlayColors[0]) + } else { + Modifier.matchParentSize().background( + Brush.linearGradient(colors = overlayColors) + ) + } + } else { + Modifier.matchParentSize().background(Color(0xFF0D8CF4)) + } + Box(modifier = lightBgModifier) + return + } + + // Если выбран цвет в Appearance — просто сплошной цвет/градиент, без blur + if (overlayColors != null && overlayColors.isNotEmpty()) { + val bgModifier = if (overlayColors.size == 1) { + Modifier.matchParentSize().background(overlayColors[0]) + } else { + Modifier.matchParentSize().background( + Brush.linearGradient(colors = overlayColors) + ) + } + Box(modifier = bgModifier) + return + } + + // Нет фона (avatar default) — blur аватарки + val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState() ?: remember { mutableStateOf(emptyList()) } - - // Состояние для bitmap и размытого bitmap + var originalBitmap by remember(avatars) { mutableStateOf(null) } var blurredBitmap by remember(avatars) { mutableStateOf(null) } - - // Декодируем и размываем аватар + LaunchedEffect(avatars) { if (avatars.isNotEmpty()) { originalBitmap = withContext(Dispatchers.IO) { AvatarFileManager.base64ToBitmap(avatars.first().base64Data) } - - // Размываем bitmap (уменьшаем для производительности, затем применяем blur) originalBitmap?.let { bitmap -> blurredBitmap = withContext(Dispatchers.Default) { - // Уменьшаем размер для быстрого blur val scaledBitmap = Bitmap.createScaledBitmap( bitmap, bitmap.width / 4, bitmap.height / 4, true ) - // Применяем blur несколько раз для более гладкого эффекта var result = scaledBitmap - repeat(3) { + repeat(2) { result = fastBlur(result, (blurRadius / 4).toInt().coerceAtLeast(1)) } result @@ -79,71 +105,22 @@ fun BoxScope.BlurredAvatarBackground( blurredBitmap = null } } - - // Используем matchParentSize() чтобы занимать только размер родителя + Box(modifier = Modifier.matchParentSize()) { if (blurredBitmap != null) { - // Показываем размытое изображение Image( bitmap = blurredBitmap!!.asImageBitmap(), contentDescription = null, - modifier = Modifier - .fillMaxSize() - .graphicsLayer { - this.alpha = alpha - }, + modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) - - // Дополнительный overlay — кастомный или стандартный - if (overlayColors != null && overlayColors.isNotEmpty()) { - // Кастомный цветной overlay - val overlayModifier = if (overlayColors.size == 1) { - Modifier - .fillMaxSize() - .background(overlayColors[0].copy(alpha = 0.55f)) - } else { - Modifier - .fillMaxSize() - .background( - Brush.linearGradient( - colors = overlayColors.map { it.copy(alpha = 0.55f) } - ) - ) - } - Box(modifier = overlayModifier) - } else { - // Стандартный overlay для затемнения - Box( - modifier = Modifier - .fillMaxSize() - .background(fallbackColor.copy(alpha = 0.3f)) - ) - } } else { - // Fallback: когда нет аватарки - if (overlayColors != null && overlayColors.isNotEmpty()) { - // Кастомный фон без blur - val bgModifier = if (overlayColors.size == 1) { - Modifier - .fillMaxSize() - .background(overlayColors[0]) - } else { - Modifier - .fillMaxSize() - .background( - Brush.linearGradient(colors = overlayColors) - ) - } - Box(modifier = bgModifier) - } else { - // Стандартный fallback: цветной фон - Box( - modifier = Modifier - .fillMaxSize() - .background(fallbackColor) - ) - } + // Нет фото — цвет аватарки + Box( + modifier = Modifier + .fillMaxSize() + .background(fallbackColor) + ) } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/AppearanceColors.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/AppearanceColors.kt index 44ad56c..fa624bc 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/AppearanceColors.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/AppearanceColors.kt @@ -29,7 +29,7 @@ object BackgroundBlurPresets { /** Сплошные цвета */ private val solidColors = listOf( - BackgroundBlurOption("solid_blue", listOf(Color(0xFF2979FF)), "Blue"), + BackgroundBlurOption("solid_blue", listOf(Color(0xFF0D8CF4)), "Blue"), BackgroundBlurOption("solid_green", listOf(Color(0xFF4CAF50)), "Green"), BackgroundBlurOption("solid_orange", listOf(Color(0xFFFF9800)), "Orange"), BackgroundBlurOption("solid_red", listOf(Color(0xFFE53935)), "Red"), diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/AppearanceScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/AppearanceScreen.kt index 9e1f408..8755152 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/AppearanceScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/AppearanceScreen.kt @@ -63,11 +63,12 @@ fun AppearanceScreen( BackHandler { onBack() } - Column( - modifier = Modifier - .fillMaxSize() - .background(backgroundColor) - ) { + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxSize() + .background(backgroundColor) + ) { // ═══════════════════════════════════════════════════════════ // CONTENT // ═══════════════════════════════════════════════════════════ @@ -105,7 +106,7 @@ fun AppearanceScreen( tint = Color.White ) } - IconButton(onClick = onToggleTheme) { + IconButton(onClick = { onToggleTheme() }) { Icon( imageVector = if (isDarkTheme) TablerIcons.Sun else TablerIcons.Moon, contentDescription = "Toggle theme", @@ -154,6 +155,7 @@ fun AppearanceScreen( Spacer(modifier = Modifier.height(32.dp)) } } + } } @@ -439,7 +441,7 @@ private fun ColorCircleItem( val borderColor by animateColorAsState( targetValue = if (isSelected) { - if (isDarkTheme) Color.White else Color(0xFF222222) + Color.White } else { Color.Transparent }, diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt index ecd8179..a82c46f 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt @@ -226,6 +226,14 @@ fun OtherProfileScreen( val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current + // Закрываем клавиатуру при открытии экрана + LaunchedEffect(Unit) { + val imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) + as android.view.inputmethod.InputMethodManager + imm.hideSoftInputFromWindow(view.windowToken, 0) + focusManager.clearFocus() + } + // 🗑️ Для удаления диалога val database = remember { RosettaDatabase.getDatabase(context) } val messageDao = remember { database.messageDao() } @@ -1664,7 +1672,7 @@ private fun CollapsingOtherProfileHeader( // ═══════════════════════════════════════════════════════════ // 🎨 TEXT COLOR - просто по теме: белый в тёмной, чёрный в светлой // ═══════════════════════════════════════════════════════════ - val textColor = if (isDarkTheme) Color.White else Color.Black + val textColor = Color.White Box(modifier = Modifier.fillMaxWidth().height(headerHeight)) { // ═══════════════════════════════════════════════════════════ @@ -1674,9 +1682,10 @@ private fun CollapsingOtherProfileHeader( publicKey = publicKey, avatarRepository = avatarRepository, fallbackColor = avatarColors.backgroundColor, - blurRadius = 25f, - alpha = 0.3f, - overlayColors = com.rosetta.messenger.ui.settings.BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId) + blurRadius = 20f, + alpha = 0.9f, + overlayColors = com.rosetta.messenger.ui.settings.BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId), + isDarkTheme = isDarkTheme ) // ═══════════════════════════════════════════════════════════ @@ -1730,7 +1739,7 @@ private fun CollapsingOtherProfileHeader( Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = "Back", - tint = if (isDarkTheme) Color.White else Color.Black, + tint = Color.White, modifier = Modifier.size(24.dp) ) } @@ -1751,7 +1760,7 @@ private fun CollapsingOtherProfileHeader( Icon( imageVector = Icons.Default.MoreVert, contentDescription = "Profile menu", - tint = if (isDarkTheme) Color.White else Color.Black, + tint = Color.White, modifier = Modifier.size(24.dp) ) } @@ -1820,12 +1829,7 @@ private fun CollapsingOtherProfileHeader( Text( text = statusText, fontSize = onlineFontSize, - color = - if (isOnline) { - Color(0xFF4CAF50) - } else { - textColor.copy(alpha = 0.7f) - } + color = Color.White ) } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt index 7feafa0..11a0aab 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt @@ -55,6 +55,8 @@ import com.rosetta.messenger.biometric.BiometricAvailability import com.rosetta.messenger.biometric.BiometricPreferences import com.rosetta.messenger.repository.AvatarRepository import com.rosetta.messenger.ui.components.BlurredAvatarBackground +import com.rosetta.messenger.ui.onboarding.PrimaryBlue +import com.rosetta.messenger.ui.onboarding.PrimaryBlueDark import com.rosetta.messenger.ui.components.metaball.ProfileMetaballEffect import com.rosetta.messenger.utils.AvatarFileManager @@ -259,7 +261,6 @@ fun ProfileScreen( onNavigateToAppearance: () -> Unit = {}, onNavigateToSafety: () -> Unit = {}, onNavigateToLogs: () -> Unit = {}, - onNavigateToCrashLogs: () -> Unit = {}, onNavigateToBiometric: () -> Unit = {}, viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel(), avatarRepository: AvatarRepository? = null, @@ -759,6 +760,31 @@ fun ProfileScreen( Spacer(modifier = Modifier.height(24.dp)) + // ═════════════════════════════════════════════════════════════ + // 🔔 NOTIFICATIONS SECTION + // ═════════════════════════════════════════════════════════════ + TelegramSectionTitle(title = "Notifications", isDarkTheme = isDarkTheme) + + run { + val preferencesManager = remember { com.rosetta.messenger.data.PreferencesManager(context) } + val notificationsEnabled by preferencesManager.notificationsEnabled.collectAsState(initial = true) + val scope = rememberCoroutineScope() + + TelegramToggleItem( + icon = TablerIcons.Bell, + title = "Push Notifications", + isEnabled = notificationsEnabled, + onToggle = { + scope.launch { + preferencesManager.setNotificationsEnabled(!notificationsEnabled) + } + }, + isDarkTheme = isDarkTheme + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + // ═════════════════════════════════════════════════════════════ // ⚙️ SETTINGS SECTION - Telegram style // ═════════════════════════════════════════════════════════════ @@ -785,14 +811,6 @@ fun ProfileScreen( title = "Safety", onClick = onNavigateToSafety, isDarkTheme = isDarkTheme, - showDivider = true - ) - - TelegramSettingsItem( - icon = TablerIcons.Bug, - title = "Crash Logs", - onClick = onNavigateToCrashLogs, - isDarkTheme = isDarkTheme, showDivider = biometricAvailable is BiometricAvailability.Available ) @@ -938,7 +956,7 @@ private fun CollapsingProfileHeader( // ═══════════════════════════════════════════════════════════ // 🎨 TEXT COLOR - просто по теме: белый в тёмной, чёрный в светлой // ═══════════════════════════════════════════════════════════ - val textColor = if (isDarkTheme) Color.White else Color.Black + val textColor = Color.White // ═══════════════════════════════════════════════════════════ // 📐 HEADER HEIGHT - ФИКСИРОВАННАЯ! Не меняется при overscroll @@ -959,7 +977,7 @@ private fun CollapsingProfileHeader( // ═══════════════════════════════════════════════════════════ // 📝 TEXT - внизу header зоны, внутри блока // ═══════════════════════════════════════════════════════════ - val textDefaultY = expandedHeight - 48.dp // Внизу header блока (ближе к низу) + val textDefaultY = expandedHeight - 70.dp // Ближе к аватарке val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT / 2 // Текст меняет позицию только при collapse, НЕ при overscroll @@ -977,9 +995,10 @@ private fun CollapsingProfileHeader( publicKey = publicKey, avatarRepository = avatarRepository, fallbackColor = avatarColors.backgroundColor, - blurRadius = 25f, - alpha = 0.3f, - overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId) + blurRadius = 20f, + alpha = 0.9f, + overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId), + isDarkTheme = isDarkTheme ) // ═══════════════════════════════════════════════════════════ @@ -1033,7 +1052,7 @@ private fun CollapsingProfileHeader( Icon( imageVector = TablerIcons.ArrowLeft, contentDescription = "Back", - tint = if (isDarkTheme) Color.White else Color.Black, + tint = Color.White, modifier = Modifier.size(24.dp) ) } @@ -1055,11 +1074,9 @@ private fun CollapsingProfileHeader( text = "Save", color = if (isSaveEnabled) { - if (isDarkTheme) Color.White else Color.Black + Color.White } else { - (if (isDarkTheme) Color.White else Color.Black).copy( - alpha = 0.45f - ) + Color.White.copy(alpha = 0.45f) }, fontWeight = FontWeight.SemiBold ) @@ -1074,7 +1091,7 @@ private fun CollapsingProfileHeader( Icon( imageVector = TablerIcons.DotsVertical, contentDescription = "Profile menu", - tint = if (isDarkTheme) Color.White else Color.Black, + tint = Color.White, modifier = Modifier.size(24.dp) ) } @@ -1174,7 +1191,8 @@ private fun FullSizeAvatar( } initialLoadComplete = true } else { - // Нет аватарки - помечаем загрузку завершенной + // Нет аватарки - сбрасываем bitmap и помечаем загрузку завершенной + bitmap = null initialLoadComplete = true } } @@ -1323,8 +1341,8 @@ fun ProfileCard( // ═════════════════════════════════════════════════════════════ @Composable -fun TelegramSectionTitle(title: String, isDarkTheme: Boolean) { - val textColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93) +fun TelegramSectionTitle(title: String, isDarkTheme: Boolean, color: Color? = null) { + val textColor = color ?: if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93) Text( text = title, @@ -1483,11 +1501,12 @@ private fun TelegramSettingsItem( onClick: () -> Unit, isDarkTheme: Boolean, showDivider: Boolean = false, - subtitle: String? = null + subtitle: String? = null, + iconTint: Color? = null ) { val textColor = if (isDarkTheme) Color.White else Color.Black val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) - val iconColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) + val iconColor = iconTint ?: if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0) Column { @@ -1527,85 +1546,109 @@ private fun TelegramSettingsItem( } @Composable -private fun TelegramBiometricItem(isEnabled: Boolean, onToggle: () -> Unit, isDarkTheme: Boolean) { +private fun TelegramToggleItem( + icon: ImageVector, + title: String, + subtitle: String? = null, + isEnabled: Boolean, + onToggle: () -> Unit, + isDarkTheme: Boolean, + showDivider: Boolean = false +) { val textColor = if (isDarkTheme) Color.White else Color.Black + val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) val iconColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) - val primaryBlue = Color(0xFF007AFF) + val accentColor = if (isDarkTheme) PrimaryBlueDark else PrimaryBlue + val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0) - Row( - modifier = - Modifier.fillMaxWidth() - .clickable(onClick = onToggle) - .padding(horizontal = 16.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = TablerIcons.Fingerprint, - contentDescription = null, - tint = iconColor, - modifier = Modifier.size(24.dp) - ) - - Spacer(modifier = Modifier.width(20.dp)) - - Text( - text = "Biometric Authentication", - fontSize = 16.sp, - color = textColor, - modifier = Modifier.weight(1f) - ) - - // iOS-style animated switch - val animatedThumbOffset by - animateFloatAsState( - targetValue = if (isEnabled) 1f else 0f, - animationSpec = - spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow - ), - label = "switchThumb" - ) - - val trackColor by - animateColorAsState( - targetValue = - if (isEnabled) primaryBlue - else if (isDarkTheme) Color(0xFF39393D) else Color(0xFFE9E9EA), - animationSpec = tween(300), - label = "trackColor" - ) - - Box( + Column { + Row( modifier = - Modifier.width(51.dp) - .height(31.dp) - .clip(RoundedCornerShape(15.5.dp)) - .background(trackColor) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = { onToggle() } - ) - .padding(2.dp) + Modifier.fillMaxWidth() + .clickable(onClick = onToggle) + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = iconColor, + modifier = Modifier.size(24.dp) + ) + + Spacer(modifier = Modifier.width(20.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text(text = title, fontSize = 16.sp, color = textColor) + if (subtitle != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text(text = subtitle, fontSize = 13.sp, color = secondaryTextColor) + } + } + + // Material 2 / old Telegram style switch + val thumbOffset by animateFloatAsState( + targetValue = if (isEnabled) 1f else 0f, + animationSpec = tween(durationMillis = 150), + label = "thumb" + ) + val trackColor by animateColorAsState( + targetValue = if (isEnabled) accentColor.copy(alpha = 0.5f) + else if (isDarkTheme) Color(0xFF39393D) else Color(0xFFBDBDBD), + animationSpec = tween(durationMillis = 150), + label = "track" + ) + val thumbColor by animateColorAsState( + targetValue = if (isEnabled) accentColor + else if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFFF1F1F1), + animationSpec = tween(durationMillis = 150), + label = "thumbColor" + ) Box( - modifier = - Modifier.size(27.dp) - .align(Alignment.CenterStart) - .offset(x = (20.dp * animatedThumbOffset)) - .shadow( - elevation = if (isEnabled) 3.dp else 2.dp, - shape = CircleShape, - spotColor = Color.Black.copy(alpha = 0.15f) - ) - .clip(CircleShape) - .background(Color.White) + modifier = Modifier + .width(37.dp) + .height(20.dp) + .clip(RoundedCornerShape(10.dp)) + .background(trackColor) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = onToggle + ), + contentAlignment = Alignment.CenterStart + ) { + Box( + modifier = Modifier + .offset(x = (17.dp * thumbOffset)) + .size(20.dp) + .shadow(2.dp, CircleShape) + .clip(CircleShape) + .background(thumbColor) + ) + } + } + + if (showDivider) { + Divider( + color = dividerColor, + thickness = 0.5.dp, + modifier = Modifier.padding(start = 60.dp) ) } } } +@Composable +private fun TelegramBiometricItem(isEnabled: Boolean, onToggle: () -> Unit, isDarkTheme: Boolean) { + TelegramToggleItem( + icon = TablerIcons.Fingerprint, + title = "Biometric Authentication", + isEnabled = isEnabled, + onToggle = onToggle, + isDarkTheme = isDarkTheme + ) +} + @Composable private fun TelegramLogoutItem(onClick: () -> Unit, isDarkTheme: Boolean) { val redColor = if (isDarkTheme) Color(0xFFFF5555) else Color(0xFFFF3B30) diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/SafetyScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/SafetyScreen.kt index 72a0979..5c08bb8 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/SafetyScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/SafetyScreen.kt @@ -24,6 +24,8 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.rosetta.messenger.ui.onboarding.PrimaryBlue +import com.rosetta.messenger.ui.onboarding.PrimaryBlueDark import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -46,10 +48,11 @@ fun SafetyScreen( val clipboardManager = LocalClipboardManager.current val context = LocalContext.current val scope = rememberCoroutineScope() - + // Copy states var copiedPublicKey by remember { mutableStateOf(false) } var copiedPrivateKey by remember { mutableStateOf(false) } + var showDeleteConfirmation by remember { mutableStateOf(false) } // Handle back gesture BackHandler { onBack() } @@ -105,7 +108,7 @@ fun SafetyScreen( // ═══════════════════════════════════════════════════════════════ // Keys Section - Telegram style // ═══════════════════════════════════════════════════════════════ - TelegramSectionHeader("Keys", secondaryTextColor) + TelegramSectionHeader("Keys", Color(0xFF8E8E93)) TelegramCopyRow( label = "Public Key", @@ -173,7 +176,7 @@ fun SafetyScreen( TelegramActionRow( label = "Delete Account", - onClick = onDeleteAccount, + onClick = { showDeleteConfirmation = true }, textColor = redColor, secondaryTextColor = secondaryTextColor, showDivider = false @@ -187,6 +190,40 @@ fun SafetyScreen( Spacer(modifier = Modifier.height(32.dp)) } } + + // Delete Account Confirmation Dialog + if (showDeleteConfirmation) { + AlertDialog( + onDismissRequest = { showDeleteConfirmation = false }, + containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, + title = { + Text( + "Delete Account", + fontWeight = FontWeight.Bold, + color = textColor + ) + }, + text = { + Text( + "You are attempting to delete your account. Are you sure? This action cannot be undone.", + color = secondaryTextColor + ) + }, + confirmButton = { + TextButton( + onClick = { + showDeleteConfirmation = false + onDeleteAccount() + } + ) { Text("Delete", color = Color(0xFFFF3B30)) } + }, + dismissButton = { + TextButton(onClick = { showDeleteConfirmation = false }) { + Text("Cancel", color = if (isDarkTheme) PrimaryBlueDark else PrimaryBlue) + } + } + ) + } } @Composable