809 lines
39 KiB
Kotlin
809 lines
39 KiB
Kotlin
package com.rosetta.messenger
|
||
|
||
import android.Manifest
|
||
import android.content.pm.PackageManager
|
||
import android.os.Build
|
||
import android.os.Bundle
|
||
import android.util.Log
|
||
import androidx.activity.ComponentActivity
|
||
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.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.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.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.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.SearchScreen
|
||
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
|
||
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
|
||
import com.rosetta.messenger.ui.settings.BackupScreen
|
||
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 : ComponentActivity() {
|
||
private lateinit var preferencesManager: PreferencesManager
|
||
private lateinit var accountManager: AccountManager
|
||
|
||
companion object {
|
||
private const val TAG = "MainActivity"
|
||
|
||
// 🔔 FCM Логи для отображения в UI
|
||
private val _fcmLogs = mutableStateListOf<String>()
|
||
val fcmLogs: List<String>
|
||
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)
|
||
}
|
||
Log.d(TAG, "FCM: $message")
|
||
}
|
||
|
||
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 isDarkTheme by preferencesManager.isDarkTheme.collectAsState(initial = 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<Boolean?>(null) }
|
||
var currentAccount by remember { mutableStateOf<DecryptedAccount?>(null) }
|
||
var accountInfoList by remember { mutableStateOf<List<AccountInfo>>(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 {
|
||
preferencesManager.setDarkTheme(!isDarkTheme)
|
||
}
|
||
},
|
||
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,
|
||
onToggleTheme = {
|
||
scope.launch {
|
||
preferencesManager.setDarkTheme(!isDarkTheme)
|
||
}
|
||
},
|
||
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}")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun MainScreen(
|
||
account: DecryptedAccount? = null,
|
||
isDarkTheme: Boolean = true,
|
||
onToggleTheme: () -> 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
|
||
Log.d("MainActivity", "Loaded from DB: name=$accountName, username=$accountUsername")
|
||
}
|
||
}
|
||
|
||
// Состояние протокола для передачи в SearchScreen
|
||
val protocolState by ProtocolManager.state.collectAsState()
|
||
|
||
// Навигация между экранами
|
||
var selectedUser by remember { mutableStateOf<SearchUser?>(null) }
|
||
var showSearchScreen by remember { mutableStateOf(false) }
|
||
var showProfileScreen by remember { mutableStateOf(false) }
|
||
var showOtherProfileScreen by remember { mutableStateOf(false) }
|
||
var selectedOtherUser by remember { mutableStateOf<SearchUser?>(null) }
|
||
|
||
// Дополнительные экраны настроек
|
||
var showUpdatesScreen by remember { mutableStateOf(false) }
|
||
var showThemeScreen by remember { mutableStateOf(false) }
|
||
var showSafetyScreen by remember { mutableStateOf(false) }
|
||
var showBackupScreen by remember { mutableStateOf(false) }
|
||
var showLogsScreen by remember { mutableStateOf(false) }
|
||
|
||
// ProfileViewModel для логов
|
||
val profileViewModel: com.rosetta.messenger.ui.settings.ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
|
||
val profileState by profileViewModel.state.collectAsState()
|
||
|
||
// Coroutine scope for profile updates
|
||
val mainScreenScope = rememberCoroutineScope()
|
||
|
||
// 🔥 Простая навигация с fade-in анимацией
|
||
Box(modifier = Modifier.fillMaxSize()) {
|
||
// Base layer - chats list
|
||
androidx.compose.animation.AnimatedVisibility(
|
||
visible = !showBackupScreen && !showSafetyScreen && !showThemeScreen &&
|
||
!showUpdatesScreen && selectedUser == null && !showSearchScreen &&
|
||
!showProfileScreen && !showOtherProfileScreen && !showLogsScreen,
|
||
enter = fadeIn(animationSpec = tween(300)),
|
||
exit = fadeOut(animationSpec = tween(200))
|
||
) {
|
||
ChatsListScreen(
|
||
isDarkTheme = isDarkTheme,
|
||
accountName = accountName,
|
||
accountUsername = accountUsername,
|
||
accountPhone = accountPhone,
|
||
accountPublicKey = accountPublicKey,
|
||
accountPrivateKey = accountPrivateKey,
|
||
privateKeyHash = privateKeyHash,
|
||
onToggleTheme = onToggleTheme,
|
||
onProfileClick = {
|
||
showProfileScreen = true
|
||
},
|
||
onNewGroupClick = {
|
||
// TODO: Navigate to new group
|
||
},
|
||
onContactsClick = {
|
||
// TODO: Navigate to contacts
|
||
},
|
||
onCallsClick = {
|
||
// TODO: Navigate to calls
|
||
},
|
||
onSavedMessagesClick = {
|
||
// Открываем чат с самим собой (Saved Messages)
|
||
selectedUser =
|
||
SearchUser(
|
||
title = "Saved Messages",
|
||
username = "",
|
||
publicKey = accountPublicKey,
|
||
verified = 0,
|
||
online = 1
|
||
)
|
||
},
|
||
onSettingsClick = { showProfileScreen = true },
|
||
onInviteFriendsClick = {
|
||
// TODO: Share invite link
|
||
},
|
||
onSearchClick = { showSearchScreen = true },
|
||
onNewChat = {
|
||
// TODO: Show new chat screen
|
||
},
|
||
onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser },
|
||
onLogout = onLogout
|
||
)
|
||
}
|
||
|
||
// Other screens with fade animation
|
||
androidx.compose.animation.AnimatedVisibility(
|
||
visible = showBackupScreen,
|
||
enter = fadeIn(animationSpec = tween(300)),
|
||
exit = fadeOut(animationSpec = tween(200))
|
||
) {
|
||
if (showBackupScreen) {
|
||
BackupScreen(
|
||
isDarkTheme = isDarkTheme,
|
||
onBack = {
|
||
showBackupScreen = false
|
||
showSafetyScreen = true
|
||
},
|
||
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) {
|
||
Log.e("MainActivity", "Error verifying password", e)
|
||
null
|
||
}
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
androidx.compose.animation.AnimatedVisibility(
|
||
visible = showSafetyScreen,
|
||
enter = fadeIn(animationSpec = tween(300)),
|
||
exit = fadeOut(animationSpec = tween(200))
|
||
) {
|
||
if (showSafetyScreen) {
|
||
SafetyScreen(
|
||
isDarkTheme = isDarkTheme,
|
||
accountPublicKey = accountPublicKey,
|
||
accountPrivateKey = accountPrivateKey,
|
||
onBack = {
|
||
showSafetyScreen = false
|
||
showProfileScreen = true
|
||
},
|
||
onBackupClick = {
|
||
showSafetyScreen = false
|
||
showBackupScreen = true
|
||
},
|
||
onDeleteAccount = {
|
||
// TODO: Implement account deletion
|
||
Log.d("MainActivity", "Delete account requested")
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
androidx.compose.animation.AnimatedVisibility(
|
||
visible = showThemeScreen,
|
||
enter = fadeIn(animationSpec = tween(300)),
|
||
exit = fadeOut(animationSpec = tween(200))
|
||
) {
|
||
if (showThemeScreen) {
|
||
ThemeScreen(
|
||
isDarkTheme = isDarkTheme,
|
||
onBack = {
|
||
showThemeScreen = false
|
||
showProfileScreen = true
|
||
},
|
||
onThemeChange = { isDark ->
|
||
onToggleTheme()
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
androidx.compose.animation.AnimatedVisibility(
|
||
visible = showUpdatesScreen,
|
||
enter = fadeIn(animationSpec = tween(300)),
|
||
exit = fadeOut(animationSpec = tween(200))
|
||
) {
|
||
if (showUpdatesScreen) {
|
||
UpdatesScreen(
|
||
isDarkTheme = isDarkTheme,
|
||
onBack = { showUpdatesScreen = false }
|
||
)
|
||
}
|
||
}
|
||
|
||
androidx.compose.animation.AnimatedVisibility(
|
||
visible = selectedUser != null,
|
||
enter = fadeIn(animationSpec = tween(300)),
|
||
exit = fadeOut(animationSpec = tween(200))
|
||
) {
|
||
if (selectedUser != null) {
|
||
// Экран чата
|
||
ChatDetailScreen(
|
||
user = selectedUser!!,
|
||
currentUserPublicKey = accountPublicKey,
|
||
currentUserPrivateKey = accountPrivateKey,
|
||
isDarkTheme = isDarkTheme,
|
||
onBack = { selectedUser = null },
|
||
onUserProfileClick = { user ->
|
||
// Открываем профиль другого пользователя
|
||
selectedOtherUser = user
|
||
showOtherProfileScreen = true
|
||
},
|
||
onNavigateToChat = { forwardUser ->
|
||
// 📨 Forward: переход в выбранный чат с полными данными
|
||
selectedUser = forwardUser
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
androidx.compose.animation.AnimatedVisibility(
|
||
visible = showSearchScreen,
|
||
enter = fadeIn(animationSpec = tween(300)),
|
||
exit = fadeOut(animationSpec = tween(200))
|
||
) {
|
||
if (showSearchScreen) {
|
||
// Экран поиска
|
||
SearchScreen(
|
||
privateKeyHash = privateKeyHash,
|
||
currentUserPublicKey = accountPublicKey,
|
||
isDarkTheme = isDarkTheme,
|
||
protocolState = protocolState,
|
||
onBackClick = { showSearchScreen = false },
|
||
onUserSelect = { selectedSearchUser ->
|
||
showSearchScreen = false
|
||
selectedUser = selectedSearchUser
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
androidx.compose.animation.AnimatedVisibility(
|
||
visible = showProfileScreen,
|
||
enter = fadeIn(animationSpec = tween(300)),
|
||
exit = fadeOut(animationSpec = tween(200))
|
||
) {
|
||
if (showProfileScreen) {
|
||
// Экран профиля
|
||
ProfileScreen(
|
||
isDarkTheme = isDarkTheme,
|
||
accountName = accountName,
|
||
accountUsername = accountUsername,
|
||
accountPublicKey = accountPublicKey,
|
||
accountPrivateKeyHash = privateKeyHash,
|
||
onBack = { showProfileScreen = false },
|
||
onSaveProfile = { name, username ->
|
||
// Following desktop version pattern:
|
||
// 1. Server confirms save (handled in ProfileViewModel)
|
||
// 2. Local DB updated (handled in ProfileScreen LaunchedEffect)
|
||
// 3. This callback updates UI state immediately
|
||
accountName = name
|
||
accountUsername = username
|
||
// Reload account list so auth screen shows updated name
|
||
mainScreenScope.launch {
|
||
onAccountInfoUpdated()
|
||
}
|
||
Log.d("MainActivity", "Profile saved: name=$name, username=$username, UI updated")
|
||
},
|
||
onLogout = onLogout,
|
||
onNavigateToTheme = {
|
||
showProfileScreen = false
|
||
showThemeScreen = true
|
||
},
|
||
onNavigateToSafety = {
|
||
showProfileScreen = false
|
||
showSafetyScreen = true
|
||
},
|
||
onNavigateToLogs = {
|
||
showProfileScreen = false
|
||
showLogsScreen = true
|
||
},
|
||
viewModel = profileViewModel
|
||
)
|
||
}
|
||
}
|
||
|
||
androidx.compose.animation.AnimatedVisibility(
|
||
visible = showLogsScreen,
|
||
enter = fadeIn(animationSpec = tween(300)),
|
||
exit = fadeOut(animationSpec = tween(200))
|
||
) {
|
||
if (showLogsScreen) {
|
||
com.rosetta.messenger.ui.settings.ProfileLogsScreen(
|
||
isDarkTheme = isDarkTheme,
|
||
logs = profileState.logs,
|
||
onBack = {
|
||
showLogsScreen = false
|
||
showProfileScreen = true
|
||
},
|
||
onClearLogs = {
|
||
profileViewModel.clearLogs()
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
androidx.compose.animation.AnimatedVisibility(
|
||
visible = showOtherProfileScreen,
|
||
enter = fadeIn(animationSpec = tween(300)),
|
||
exit = fadeOut(animationSpec = tween(200))
|
||
) {
|
||
if (showOtherProfileScreen && selectedOtherUser != null) {
|
||
OtherProfileScreen(
|
||
user = selectedOtherUser!!,
|
||
isDarkTheme = isDarkTheme,
|
||
onBack = {
|
||
showOtherProfileScreen = false
|
||
selectedOtherUser = null
|
||
}
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|