package com.rosetta.messenger import android.Manifest import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.* import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import androidx.lifecycle.lifecycleScope import com.google.firebase.FirebaseApp import com.google.firebase.messaging.FirebaseMessaging import com.rosetta.messenger.data.AccountManager import com.rosetta.messenger.data.DecryptedAccount import com.rosetta.messenger.data.PreferencesManager import com.rosetta.messenger.data.RecentSearchesManager import com.rosetta.messenger.database.RosettaDatabase import com.rosetta.messenger.network.PacketPushNotification import com.rosetta.messenger.network.ProtocolManager import com.rosetta.messenger.network.ProtocolState import com.rosetta.messenger.network.PushNotificationAction import com.rosetta.messenger.network.SearchUser import com.rosetta.messenger.repository.AvatarRepository import com.rosetta.messenger.ui.auth.AccountInfo import com.rosetta.messenger.ui.auth.AuthFlow import com.rosetta.messenger.ui.chats.ChatDetailScreen import com.rosetta.messenger.ui.chats.ChatsListScreen import com.rosetta.messenger.ui.chats.RequestsListScreen import com.rosetta.messenger.ui.chats.SearchScreen import com.rosetta.messenger.ui.components.OptimizedEmojiCache import com.rosetta.messenger.ui.components.SwipeBackContainer import com.rosetta.messenger.ui.crashlogs.CrashLogsScreen import com.rosetta.messenger.ui.onboarding.OnboardingScreen import com.rosetta.messenger.ui.settings.BackupScreen import com.rosetta.messenger.ui.settings.BiometricEnableScreen import com.rosetta.messenger.ui.settings.OtherProfileScreen import com.rosetta.messenger.ui.settings.ProfileScreen import com.rosetta.messenger.ui.settings.SafetyScreen import com.rosetta.messenger.ui.settings.ThemeScreen import com.rosetta.messenger.ui.settings.UpdatesScreen import com.rosetta.messenger.ui.splash.SplashScreen import com.rosetta.messenger.ui.theme.RosettaAndroidTheme import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import kotlinx.coroutines.delay import kotlinx.coroutines.launch class MainActivity : FragmentActivity() { private lateinit var preferencesManager: PreferencesManager private lateinit var accountManager: AccountManager companion object { private const val TAG = "MainActivity" // 🔔 FCM Логи для отображения в UI private val _fcmLogs = mutableStateListOf() val fcmLogs: List get() = _fcmLogs fun addFcmLog(message: String) { val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date()) _fcmLogs.add(0, "[$timestamp] $message") // Добавляем в начало списка // Ограничиваем количество логов if (_fcmLogs.size > 20) { _fcmLogs.removeAt(_fcmLogs.size - 1) } } fun clearFcmLogs() { _fcmLogs.clear() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() preferencesManager = PreferencesManager(this) accountManager = AccountManager(this) RecentSearchesManager.init(this) // 🔥 Инициализируем ProtocolManager для обработки онлайн статусов ProtocolManager.initialize(this) // 🔔 Инициализируем Firebase для push-уведомлений initializeFirebase() // 🔥 Помечаем что приложение в foreground com.rosetta.messenger.push.RosettaFirebaseMessagingService.isAppInForeground = true // 📱 Предзагружаем эмодзи в фоне для мгновенного открытия пикера // Используем новый оптимизированный кэш OptimizedEmojiCache.preload(this) setContent { // 🔔 Запрос разрешения на уведомления для Android 13+ val notificationPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission(), onResult = { isGranted -> } ) // Запрашиваем разрешение при первом запуске (Android 13+) LaunchedEffect(Unit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val hasPermission = ContextCompat.checkSelfPermission( this@MainActivity, Manifest.permission.POST_NOTIFICATIONS ) == PackageManager.PERMISSION_GRANTED if (!hasPermission) { notificationPermissionLauncher.launch( Manifest.permission.POST_NOTIFICATIONS ) } } } val scope = rememberCoroutineScope() val themeMode by preferencesManager.themeMode.collectAsState(initial = "dark") val systemInDarkTheme = isSystemInDarkTheme() val isDarkTheme = when (themeMode) { "light" -> false "dark" -> true "auto" -> systemInDarkTheme else -> true } val isLoggedIn by accountManager.isLoggedIn.collectAsState(initial = null) var showSplash by remember { mutableStateOf(true) } var showOnboarding by remember { mutableStateOf(true) } var hasExistingAccount by remember { mutableStateOf(null) } var currentAccount by remember { mutableStateOf(null) } var accountInfoList by remember { mutableStateOf>(emptyList()) } // Check for existing accounts and build AccountInfo list // Also force logout so user always sees unlock screen on app restart LaunchedEffect(Unit) { accountManager.logout() // Always start logged out val accounts = accountManager.getAllAccounts() hasExistingAccount = accounts.isNotEmpty() accountInfoList = accounts.map { account -> val shortKey = account.publicKey.take(7) val displayName = account.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 = account.publicKey, name = displayName, username = account.username ?: "", initials = initials, publicKey = account.publicKey ) } } // Wait for initial load if (hasExistingAccount == null) { Box( modifier = Modifier.fillMaxSize() .background( if (isDarkTheme) Color(0xFF1B1B1B) else Color.White ) ) return@setContent } RosettaAndroidTheme(darkTheme = isDarkTheme, animated = true) { Surface( modifier = Modifier.fillMaxSize(), color = if (isDarkTheme) Color(0xFF1B1B1B) else Color.White ) { AnimatedContent( targetState = when { showSplash -> "splash" showOnboarding && hasExistingAccount == false -> "onboarding" isLoggedIn != true && hasExistingAccount == false -> "auth_new" isLoggedIn != true && hasExistingAccount == true -> "auth_unlock" else -> "main" }, transitionSpec = { fadeIn(animationSpec = tween(600)) togetherWith fadeOut(animationSpec = tween(600)) }, label = "screenTransition" ) { screen -> when (screen) { "splash" -> { SplashScreen( isDarkTheme = isDarkTheme, onSplashComplete = { showSplash = false } ) } "onboarding" -> { OnboardingScreen( isDarkTheme = isDarkTheme, onThemeToggle = { scope.launch { val newMode = if (isDarkTheme) "light" else "dark" preferencesManager.setThemeMode(newMode) } }, onStartMessaging = { showOnboarding = false } ) } "auth_new", "auth_unlock" -> { AuthFlow( isDarkTheme = isDarkTheme, hasExistingAccount = screen == "auth_unlock", accounts = accountInfoList, accountManager = accountManager, onAuthComplete = { account -> currentAccount = account hasExistingAccount = true // Save as last logged account account?.let { accountManager.setLastLoggedPublicKey(it.publicKey) } // 📤 Отправляем FCM токен на сервер после успешной // аутентификации account?.let { sendFcmTokenToServer(it) } // Reload accounts list scope.launch { val accounts = accountManager.getAllAccounts() accountInfoList = accounts.map { acc -> 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 ) } } }, onLogout = { // Set currentAccount to null immediately to prevent UI // lag currentAccount = null scope.launch { com.rosetta.messenger.network.ProtocolManager .disconnect() accountManager.logout() } } ) } "main" -> { MainScreen( account = currentAccount, isDarkTheme = isDarkTheme, themeMode = themeMode, onToggleTheme = { scope.launch { val newMode = if (isDarkTheme) "light" else "dark" preferencesManager.setThemeMode(newMode) } }, onThemeModeChange = { mode -> scope.launch { preferencesManager.setThemeMode(mode) } }, onLogout = { // Set currentAccount to null immediately to prevent UI // lag currentAccount = null scope.launch { com.rosetta.messenger.network.ProtocolManager .disconnect() accountManager.logout() } }, onAccountInfoUpdated = { // Reload account list when profile is updated val accounts = accountManager.getAllAccounts() accountInfoList = accounts.map { acc -> 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 ) } } ) } } } } } } } override fun onResume() { super.onResume() // 🔥 Приложение стало видимым - отключаем уведомления com.rosetta.messenger.push.RosettaFirebaseMessagingService.isAppInForeground = true } override fun onPause() { super.onPause() // 🔥 Приложение ушло в background - включаем уведомления com.rosetta.messenger.push.RosettaFirebaseMessagingService.isAppInForeground = false } /** 🔔 Инициализация Firebase Cloud Messaging */ private fun initializeFirebase() { try { addFcmLog("🔔 Инициализация Firebase...") // Инициализируем Firebase FirebaseApp.initializeApp(this) addFcmLog("✅ Firebase инициализирован") // Получаем FCM токен addFcmLog("📲 Запрос FCM токена...") FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> if (!task.isSuccessful) { addFcmLog("❌ Ошибка получения токена: ${task.exception?.message}") return@addOnCompleteListener } val token = task.result if (token != null) { val shortToken = "${token.take(12)}...${token.takeLast(8)}" addFcmLog("✅ FCM токен получен: $shortToken") // Сохраняем токен локально saveFcmToken(token) addFcmLog("💾 Токен сохранен локально") } else { addFcmLog("⚠️ Токен пустой") } // Токен будет отправлен на сервер после успешной аутентификации // (см. вызов sendFcmTokenToServer в onAccountLogin) } } catch (e: Exception) { addFcmLog("❌ Ошибка Firebase: ${e.message}") } } /** Сохранить FCM токен в SharedPreferences */ private fun saveFcmToken(token: String) { val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE) prefs.edit().putString("fcm_token", token).apply() } /** * Отправить FCM токен на сервер Вызывается после успешной аутентификации, когда аккаунт уже * расшифрован */ private fun sendFcmTokenToServer(account: DecryptedAccount) { lifecycleScope.launch { try { val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE) val token = prefs.getString("fcm_token", null) if (token == null) { addFcmLog("⚠️ Нет сохраненного токена для отправки") return@launch } val shortToken = "${token.take(12)}...${token.takeLast(8)}" addFcmLog("📤 Подготовка к отправке токена на сервер") addFcmLog("⏳ Ожидание аутентификации...") // 🔥 КРИТИЧНО: Ждем пока протокол станет AUTHENTICATED var waitAttempts = 0 while (ProtocolManager.state.value != ProtocolState.AUTHENTICATED && waitAttempts < 50) { delay(100) // Ждем 100ms waitAttempts++ } if (ProtocolManager.state.value != ProtocolState.AUTHENTICATED) { addFcmLog("❌ Таймаут аутентификации (${waitAttempts * 100}ms)") return@launch } addFcmLog("✅ Аутентификация успешна") addFcmLog("📨 Отправка токена: $shortToken") val packet = PacketPushNotification().apply { this.notificationsToken = token this.action = PushNotificationAction.SUBSCRIBE } ProtocolManager.send(packet) addFcmLog("✅ Пакет отправлен на сервер (ID: 0x10)") addFcmLog("🎉 FCM токен успешно зарегистрирован!") } catch (e: Exception) { addFcmLog("❌ Ошибка отправки: ${e.message}") } } } } /** * Navigation sealed class — replaces ~15 boolean flags with a type-safe navigation stack. ChatsList * is always the base layer and is not part of the stack. Each SwipeBackContainer reads a * derivedStateOf, so pushing/popping an unrelated screen won't trigger recomposition of other * screens. */ sealed class Screen { data object Profile : Screen() data object Requests : Screen() data object Search : Screen() data class ChatDetail(val user: SearchUser) : Screen() data class OtherProfile(val user: SearchUser) : Screen() data object Updates : Screen() data object Theme : Screen() data object Safety : Screen() data object Backup : Screen() data object Logs : Screen() data object CrashLogs : Screen() data object Biometric : Screen() data object Appearance : Screen() } @Composable fun MainScreen( account: DecryptedAccount? = null, isDarkTheme: Boolean = true, themeMode: String = "dark", onToggleTheme: () -> Unit = {}, onThemeModeChange: (String) -> Unit = {}, onLogout: () -> Unit = {}, onAccountInfoUpdated: suspend () -> Unit = {} ) { // Reactive state for account name and username var accountName by remember { mutableStateOf(account?.name ?: "Account") } val accountPhone = account?.publicKey?.take(16)?.let { "+${it.take(1)} ${it.substring(1, 4)} ${it.substring(4, 7)}${it.substring(7)}" } ?: "+7 775 9932587" val accountPublicKey = account?.publicKey ?: "04c266b98ae5" val accountPrivateKey = account?.privateKey ?: "" val privateKeyHash = account?.privateKeyHash ?: "" // Username state - загружается из EncryptedAccount // Following desktop version pattern: username is stored locally and loaded on app start var accountUsername by remember { mutableStateOf("") } var reloadTrigger by remember { mutableIntStateOf(0) } // Load username AND name from AccountManager (persisted in DataStore) val context = LocalContext.current LaunchedEffect(accountPublicKey, reloadTrigger) { if (accountPublicKey.isNotBlank() && accountPublicKey != "04c266b98ae5") { val accountManager = AccountManager(context) val encryptedAccount = accountManager.getAccount(accountPublicKey) accountUsername = encryptedAccount?.username ?: "" accountName = encryptedAccount?.name ?: accountName } } // Состояние протокола для передачи в SearchScreen val protocolState by ProtocolManager.state.collectAsState() // Перечитать username/name после получения own profile с сервера // Аналог Desktop: useUserInformation автоматически обновляет UI при PacketSearch ответе LaunchedEffect(protocolState) { if (protocolState == ProtocolState.AUTHENTICATED && accountPublicKey.isNotBlank() && accountPublicKey != "04c266b98ae5" ) { delay(2000) // Ждём fetchOwnProfile() → PacketSearch → AccountManager update val accountManager = AccountManager(context) val encryptedAccount = accountManager.getAccount(accountPublicKey) accountUsername = encryptedAccount?.username ?: "" accountName = encryptedAccount?.name ?: accountName } } // ═══════════════════════════════════════════════════════════ // Navigation stack — sealed class instead of ~15 boolean flags. // ChatsList is always the base layer (not in stack). // Each derivedStateOf only recomposes its SwipeBackContainer // when that specific screen appears/disappears — not on every // navigation change. This eliminates the massive recomposition // that happened when ANY boolean flag changed. // ═══════════════════════════════════════════════════════════ var navStack by remember { mutableStateOf>(emptyList()) } // Derived visibility — only triggers recomposition when THIS screen changes val isProfileVisible by remember { derivedStateOf { navStack.any { it is Screen.Profile } } } val isRequestsVisible by remember { derivedStateOf { navStack.any { it is Screen.Requests } } } val isSearchVisible by remember { derivedStateOf { navStack.any { it is Screen.Search } } } val chatDetailScreen by remember { derivedStateOf { navStack.filterIsInstance().lastOrNull() } } val selectedUser = chatDetailScreen?.user val otherProfileScreen by remember { derivedStateOf { navStack.filterIsInstance().lastOrNull() } } val selectedOtherUser = otherProfileScreen?.user val isUpdatesVisible by remember { derivedStateOf { navStack.any { it is Screen.Updates } } } val isThemeVisible by remember { derivedStateOf { navStack.any { it is Screen.Theme } } } val isSafetyVisible by remember { derivedStateOf { navStack.any { it is Screen.Safety } } } val isBackupVisible by remember { derivedStateOf { navStack.any { it is Screen.Backup } } } val isLogsVisible by remember { derivedStateOf { navStack.any { it is Screen.Logs } } } val isCrashLogsVisible by remember { derivedStateOf { navStack.any { it is Screen.CrashLogs } } } val isBiometricVisible by remember { derivedStateOf { navStack.any { it is Screen.Biometric } } } val isAppearanceVisible by remember { derivedStateOf { navStack.any { it is Screen.Appearance } } } // Navigation helpers fun pushScreen(screen: Screen) { navStack = navStack + screen } fun popScreen() { navStack = navStack.dropLast(1) } fun popProfileAndChildren() { navStack = navStack.filterNot { it is Screen.Profile || it is Screen.Theme || it is Screen.Safety || it is Screen.Backup || it is Screen.Logs || it is Screen.CrashLogs || it is Screen.Biometric || it is Screen.Appearance } } fun popChatAndChildren() { navStack = navStack.filterNot { it is Screen.ChatDetail || it is Screen.OtherProfile } } // ProfileViewModel для логов val profileViewModel: com.rosetta.messenger.ui.settings.ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel() val profileState by profileViewModel.state.collectAsState() val chatsListViewModel: com.rosetta.messenger.ui.chats.ChatsListViewModel = androidx.lifecycle.viewmodel.compose.viewModel() // Appearance: background blur color preference val prefsManager = remember { com.rosetta.messenger.data.PreferencesManager(context) } val backgroundBlurColorId by prefsManager.backgroundBlurColorId.collectAsState(initial = "avatar") val pinnedChats by prefsManager.pinnedChats.collectAsState(initial = emptySet()) // AvatarRepository для работы с аватарами val avatarRepository = remember(accountPublicKey) { if (accountPublicKey.isNotBlank() && accountPublicKey != "04c266b98ae5") { val database = RosettaDatabase.getDatabase(context) AvatarRepository( context = context, avatarDao = database.avatarDao(), currentPublicKey = accountPublicKey ) } else { null } } // Coroutine scope for profile updates val mainScreenScope = rememberCoroutineScope() // 🔥 Простая навигация с swipe back Box(modifier = Modifier.fillMaxSize()) { // Base layer - chats list (всегда видимый, чтобы его было видно при свайпе) ChatsListScreen( isDarkTheme = isDarkTheme, accountName = accountName, accountUsername = accountUsername, accountPhone = accountPhone, accountPublicKey = accountPublicKey, accountPrivateKey = accountPrivateKey, privateKeyHash = privateKeyHash, onToggleTheme = onToggleTheme, onProfileClick = { pushScreen(Screen.Profile) }, onNewGroupClick = { // TODO: Navigate to new group }, onContactsClick = { // TODO: Navigate to contacts }, onCallsClick = { // TODO: Navigate to calls }, onSavedMessagesClick = { // Открываем чат с самим собой (Saved Messages) pushScreen( Screen.ChatDetail( SearchUser( title = "Saved Messages", username = "", publicKey = accountPublicKey, verified = 0, online = 1 ) ) ) }, onSettingsClick = { pushScreen(Screen.Profile) }, onInviteFriendsClick = { // TODO: Share invite link }, onSearchClick = { pushScreen(Screen.Search) }, onRequestsClick = { pushScreen(Screen.Requests) }, onNewChat = { // TODO: Show new chat screen }, onUserSelect = { selectedChatUser -> pushScreen(Screen.ChatDetail(selectedChatUser)) }, backgroundBlurColorId = backgroundBlurColorId, pinnedChats = pinnedChats, onTogglePin = { opponentKey -> mainScreenScope.launch { prefsManager.togglePinChat(opponentKey) } }, chatsViewModel = chatsListViewModel, avatarRepository = avatarRepository, onLogout = onLogout ) SwipeBackContainer( isVisible = isRequestsVisible, onBack = { navStack = navStack.filterNot { it is Screen.Requests } }, isDarkTheme = isDarkTheme ) { RequestsListScreen( isDarkTheme = isDarkTheme, chatsViewModel = chatsListViewModel, onBack = { navStack = navStack.filterNot { it is Screen.Requests } }, onUserSelect = { selectedRequestUser -> navStack = navStack.filterNot { it is Screen.Requests || it is Screen.ChatDetail } + Screen.ChatDetail(selectedRequestUser) } ) } // ═══════════════════════════════════════════════════════════ // Profile Screen — MUST be before sub-screens so it stays // visible beneath them during swipe-back animation // ═══════════════════════════════════════════════════════════ SwipeBackContainer( isVisible = isProfileVisible, onBack = { popProfileAndChildren() }, isDarkTheme = isDarkTheme ) { // Экран профиля ProfileScreen( isDarkTheme = isDarkTheme, accountName = accountName, accountUsername = accountUsername, accountPublicKey = accountPublicKey, accountPrivateKeyHash = privateKeyHash, onBack = { popProfileAndChildren() }, onSaveProfile = { name, username -> accountName = name accountUsername = username mainScreenScope.launch { onAccountInfoUpdated() } }, onLogout = onLogout, onNavigateToTheme = { pushScreen(Screen.Theme) }, onNavigateToAppearance = { pushScreen(Screen.Appearance) }, onNavigateToSafety = { pushScreen(Screen.Safety) }, onNavigateToLogs = { pushScreen(Screen.Logs) }, onNavigateToCrashLogs = { pushScreen(Screen.CrashLogs) }, onNavigateToBiometric = { pushScreen(Screen.Biometric) }, viewModel = profileViewModel, avatarRepository = avatarRepository, dialogDao = RosettaDatabase.getDatabase(context).dialogDao(), backgroundBlurColorId = backgroundBlurColorId ) } // Other screens with swipe back SwipeBackContainer( isVisible = isBackupVisible, onBack = { navStack = navStack.filterNot { it is Screen.Backup } + Screen.Safety }, isDarkTheme = isDarkTheme ) { BackupScreen( isDarkTheme = isDarkTheme, onBack = { navStack = navStack.filterNot { it is Screen.Backup } + Screen.Safety }, onVerifyPassword = { password -> // Verify password by trying to decrypt the private key try { val publicKey = account?.publicKey ?: return@BackupScreen null val accountManager = AccountManager(context) val encryptedAccount = accountManager.getAccount(publicKey) if (encryptedAccount != null) { // Try to decrypt private key with password val decryptedPrivateKey = com.rosetta.messenger.crypto.CryptoManager .decryptWithPassword( encryptedAccount.encryptedPrivateKey, password ) if (decryptedPrivateKey != null) { // Password is correct, decrypt seed phrase com.rosetta.messenger.crypto.CryptoManager.decryptWithPassword( encryptedAccount.encryptedSeedPhrase, password ) } else { null } } else { null } } catch (e: Exception) { null } } ) } SwipeBackContainer( isVisible = isSafetyVisible, onBack = { navStack = navStack.filterNot { it is Screen.Safety } }, isDarkTheme = isDarkTheme ) { SafetyScreen( isDarkTheme = isDarkTheme, accountPublicKey = accountPublicKey, accountPrivateKey = accountPrivateKey, onBack = { navStack = navStack.filterNot { it is Screen.Safety } }, onBackupClick = { navStack = navStack.filterNot { it is Screen.Safety } + Screen.Backup }, onDeleteAccount = { // TODO: Implement account deletion } ) } SwipeBackContainer( isVisible = isThemeVisible, onBack = { navStack = navStack.filterNot { it is Screen.Theme } }, isDarkTheme = isDarkTheme ) { ThemeScreen( isDarkTheme = isDarkTheme, currentThemeMode = themeMode, onBack = { navStack = navStack.filterNot { it is Screen.Theme } }, onThemeModeChange = onThemeModeChange ) } SwipeBackContainer( isVisible = isAppearanceVisible, onBack = { navStack = navStack.filterNot { it is Screen.Appearance } }, isDarkTheme = isDarkTheme ) { com.rosetta.messenger.ui.settings.AppearanceScreen( isDarkTheme = isDarkTheme, currentBlurColorId = backgroundBlurColorId, onBack = { navStack = navStack.filterNot { it is Screen.Appearance } }, onBlurColorChange = { newId -> mainScreenScope.launch { prefsManager.setBackgroundBlurColorId(newId) } }, onToggleTheme = onToggleTheme, accountPublicKey = accountPublicKey, accountName = accountName, avatarRepository = avatarRepository ) } SwipeBackContainer( isVisible = isUpdatesVisible, onBack = { navStack = navStack.filterNot { it is Screen.Updates } }, isDarkTheme = isDarkTheme ) { UpdatesScreen( isDarkTheme = isDarkTheme, onBack = { navStack = navStack.filterNot { it is Screen.Updates } } ) } // 🔒 Lock swipe-back while chat overlays are open (image viewer/editor/media picker/camera). var isChatSwipeLocked by remember { mutableStateOf(false) } SwipeBackContainer( isVisible = selectedUser != null, onBack = { popChatAndChildren() }, isDarkTheme = isDarkTheme, swipeEnabled = !isChatSwipeLocked ) { selectedUser?.let { currentChatUser -> // Экран чата ChatDetailScreen( user = currentChatUser, currentUserPublicKey = accountPublicKey, currentUserPrivateKey = accountPrivateKey, onBack = { popChatAndChildren() }, onUserProfileClick = { user -> // Открываем профиль другого пользователя pushScreen(Screen.OtherProfile(user)) }, onNavigateToChat = { forwardUser -> // 📨 Forward: переход в выбранный чат с полными данными navStack = navStack.filterNot { it is Screen.ChatDetail || it is Screen.OtherProfile } + Screen.ChatDetail(forwardUser) }, isDarkTheme = isDarkTheme, avatarRepository = avatarRepository, onImageViewerChanged = { isLocked -> isChatSwipeLocked = isLocked } ) } } SwipeBackContainer( isVisible = isSearchVisible, onBack = { navStack = navStack.filterNot { it is Screen.Search } }, isDarkTheme = isDarkTheme ) { // Экран поиска SearchScreen( privateKeyHash = privateKeyHash, currentUserPublicKey = accountPublicKey, isDarkTheme = isDarkTheme, protocolState = protocolState, onBackClick = { navStack = navStack.filterNot { it is Screen.Search } }, onUserSelect = { selectedSearchUser -> navStack = navStack.filterNot { it is Screen.Search } + Screen.ChatDetail(selectedSearchUser) } ) } SwipeBackContainer( isVisible = isLogsVisible, onBack = { navStack = navStack.filterNot { it is Screen.Logs } }, isDarkTheme = isDarkTheme ) { com.rosetta.messenger.ui.settings.ProfileLogsScreen( isDarkTheme = isDarkTheme, logs = profileState.logs, onBack = { navStack = navStack.filterNot { it is Screen.Logs } }, onClearLogs = { profileViewModel.clearLogs() } ) } SwipeBackContainer( isVisible = isCrashLogsVisible, onBack = { navStack = navStack.filterNot { it is Screen.CrashLogs } }, isDarkTheme = isDarkTheme ) { CrashLogsScreen( onBackClick = { navStack = navStack.filterNot { it is Screen.CrashLogs } } ) } var isOtherProfileSwipeEnabled by remember { mutableStateOf(true) } LaunchedEffect(selectedOtherUser?.publicKey) { isOtherProfileSwipeEnabled = true } SwipeBackContainer( isVisible = selectedOtherUser != null, onBack = { navStack = navStack.filterNot { it is Screen.OtherProfile } }, isDarkTheme = isDarkTheme, swipeEnabled = isOtherProfileSwipeEnabled ) { selectedOtherUser?.let { currentOtherUser -> OtherProfileScreen( user = currentOtherUser, isDarkTheme = isDarkTheme, onBack = { navStack = navStack.filterNot { it is Screen.OtherProfile } }, onSwipeBackEnabledChanged = { enabled -> isOtherProfileSwipeEnabled = enabled }, avatarRepository = avatarRepository, currentUserPublicKey = accountPublicKey, currentUserPrivateKey = accountPrivateKey ) } } // Biometric Enable Screen SwipeBackContainer( isVisible = isBiometricVisible, onBack = { navStack = navStack.filterNot { it is Screen.Biometric } }, isDarkTheme = isDarkTheme ) { val biometricManager = remember { com.rosetta.messenger.biometric.BiometricAuthManager(context) } val biometricPrefs = remember { com.rosetta.messenger.biometric.BiometricPreferences(context) } val activity = context as? FragmentActivity BiometricEnableScreen( isDarkTheme = isDarkTheme, onBack = { navStack = navStack.filterNot { it is Screen.Biometric } }, onEnable = { password, onSuccess, onError -> if (activity == null) { onError("Activity not available") return@BiometricEnableScreen } biometricManager.encryptPassword( activity = activity, password = password, onSuccess = { encryptedPassword -> mainScreenScope.launch { biometricPrefs.saveEncryptedPassword( accountPublicKey, encryptedPassword ) biometricPrefs.enableBiometric() onSuccess() } }, onError = { error -> onError(error) }, onCancel = { navStack = navStack.filterNot { it is Screen.Biometric } } ) } ) } } }