Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package com.rosetta.messenger
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -11,110 +10,115 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.firebase.FirebaseApp
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import com.rosetta.messenger.crypto.CryptoManager
|
||||
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.PushNotificationAction
|
||||
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.ChatsListScreen
|
||||
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.network.SearchUser
|
||||
import com.rosetta.messenger.ui.components.EmojiCache
|
||||
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
|
||||
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
|
||||
import com.rosetta.messenger.ui.splash.SplashScreen
|
||||
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
|
||||
import kotlinx.coroutines.launch
|
||||
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 ->
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
val hasPermission =
|
||||
ContextCompat.checkSelfPermission(
|
||||
this@MainActivity,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
if (!hasPermission) {
|
||||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
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)
|
||||
@@ -123,152 +127,179 @@ class MainActivity : ComponentActivity() {
|
||||
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()
|
||||
}
|
||||
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,
|
||||
initials = initials,
|
||||
publicKey = account.publicKey
|
||||
)
|
||||
}
|
||||
AccountInfo(
|
||||
id = account.publicKey,
|
||||
name = displayName,
|
||||
initials = initials,
|
||||
publicKey = account.publicKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Wait for initial load
|
||||
if (hasExistingAccount == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(if (isDarkTheme) Color(0xFF1B1B1B) else Color.White)
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.background(
|
||||
if (isDarkTheme) Color(0xFF1B1B1B) else Color.White
|
||||
)
|
||||
)
|
||||
return@setContent
|
||||
}
|
||||
|
||||
RosettaAndroidTheme(
|
||||
darkTheme = isDarkTheme,
|
||||
animated = true
|
||||
) {
|
||||
|
||||
RosettaAndroidTheme(darkTheme = isDarkTheme, animated = true) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = if (isDarkTheme) Color(0xFF1B1B1B) else Color.White
|
||||
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"
|
||||
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 }
|
||||
isDarkTheme = isDarkTheme,
|
||||
onSplashComplete = { showSplash = false }
|
||||
)
|
||||
}
|
||||
"onboarding" -> {
|
||||
OnboardingScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onThemeToggle = {
|
||||
scope.launch {
|
||||
preferencesManager.setDarkTheme(!isDarkTheme)
|
||||
}
|
||||
},
|
||||
onStartMessaging = {
|
||||
showOnboarding = false
|
||||
}
|
||||
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()
|
||||
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,
|
||||
initials = initials,
|
||||
publicKey = acc.publicKey
|
||||
)
|
||||
}
|
||||
}
|
||||
AccountInfo(
|
||||
id = acc.publicKey,
|
||||
name = displayName,
|
||||
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()
|
||||
}
|
||||
}
|
||||
},
|
||||
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)
|
||||
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()
|
||||
}
|
||||
}
|
||||
},
|
||||
onLogout = {
|
||||
// Set currentAccount to null immediately to prevent UI lag
|
||||
currentAccount = null
|
||||
scope.launch {
|
||||
com.rosetta.messenger.network.ProtocolManager.disconnect()
|
||||
accountManager.logout()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -277,86 +308,107 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
/** 🔔 Инициализация 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
|
||||
|
||||
// Сохраняем токен локально
|
||||
token?.let { saveFcmToken(it) }
|
||||
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
/** Сохранить FCM токен в SharedPreferences */
|
||||
private fun saveFcmToken(token: String) {
|
||||
val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE)
|
||||
prefs.edit().putString("fcm_token", token).apply()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Отправить FCM токен на сервер
|
||||
* Вызывается после успешной аутентификации, когда аккаунт уже расшифрован
|
||||
* Отправить 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) {
|
||||
while (ProtocolManager.state.value != ProtocolState.AUTHENTICATED &&
|
||||
waitAttempts < 50) {
|
||||
delay(100) // Ждем 100ms
|
||||
waitAttempts++
|
||||
}
|
||||
|
||||
|
||||
if (ProtocolManager.state.value != ProtocolState.AUTHENTICATED) {
|
||||
addFcmLog("❌ Таймаут аутентификации (${waitAttempts * 100}ms)")
|
||||
return@launch
|
||||
}
|
||||
|
||||
val packet = PacketPushNotification().apply {
|
||||
this.notificationsToken = token
|
||||
this.action = PushNotificationAction.SUBSCRIBE
|
||||
}
|
||||
|
||||
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,177 +416,162 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
account: DecryptedAccount? = null,
|
||||
isDarkTheme: Boolean = true,
|
||||
onToggleTheme: () -> Unit = {},
|
||||
onLogout: () -> Unit = {}
|
||||
account: DecryptedAccount? = null,
|
||||
isDarkTheme: Boolean = true,
|
||||
onToggleTheme: () -> Unit = {},
|
||||
onLogout: () -> Unit = {}
|
||||
) {
|
||||
val accountName = 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 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 ?: ""
|
||||
|
||||
|
||||
// Состояние протокола для передачи в SearchScreen
|
||||
val protocolState by ProtocolManager.state.collectAsState()
|
||||
|
||||
|
||||
// Навигация между экранами
|
||||
var selectedUser by remember { mutableStateOf<SearchUser?>(null) }
|
||||
var showSearchScreen by remember { mutableStateOf(false) }
|
||||
|
||||
|
||||
// 🔥 TELEGRAM-STYLE анимация - чистый slide БЕЗ прозрачности
|
||||
AnimatedContent(
|
||||
targetState = Triple(selectedUser, showSearchScreen, Unit),
|
||||
transitionSpec = {
|
||||
val isEnteringChat = targetState.first != null && initialState.first == null
|
||||
val isExitingChat = targetState.first == null && initialState.first != null
|
||||
val isEnteringSearch = targetState.second && !initialState.second
|
||||
val isExitingSearch = !targetState.second && initialState.second
|
||||
|
||||
when {
|
||||
// 🚀 Вход в чат - плавный fade
|
||||
isEnteringChat -> {
|
||||
fadeIn(
|
||||
animationSpec = tween(200)
|
||||
) togetherWith fadeOut(
|
||||
animationSpec = tween(150)
|
||||
)
|
||||
targetState = Triple(selectedUser, showSearchScreen, Unit),
|
||||
transitionSpec = {
|
||||
val isEnteringChat = targetState.first != null && initialState.first == null
|
||||
val isExitingChat = targetState.first == null && initialState.first != null
|
||||
val isEnteringSearch = targetState.second && !initialState.second
|
||||
val isExitingSearch = !targetState.second && initialState.second
|
||||
|
||||
when {
|
||||
// 🚀 Вход в чат - плавный fade
|
||||
isEnteringChat -> {
|
||||
fadeIn(animationSpec = tween(200)) togetherWith
|
||||
fadeOut(animationSpec = tween(150))
|
||||
}
|
||||
|
||||
// 🔙 Выход из чата - плавный fade
|
||||
isExitingChat -> {
|
||||
fadeIn(animationSpec = tween(200)) togetherWith
|
||||
fadeOut(animationSpec = tween(150))
|
||||
}
|
||||
|
||||
// 🔍 Вход в Search - плавный fade
|
||||
isEnteringSearch -> {
|
||||
fadeIn(animationSpec = tween(200)) togetherWith
|
||||
fadeOut(animationSpec = tween(150))
|
||||
}
|
||||
|
||||
// 🔙 Выход из Search - плавный fade
|
||||
isEnteringSearch -> {
|
||||
fadeIn(animationSpec = tween(200)) togetherWith
|
||||
fadeOut(animationSpec = tween(150))
|
||||
}
|
||||
|
||||
// 🔙 Выход из Search - плавный fade
|
||||
isExitingSearch -> {
|
||||
fadeIn(animationSpec = tween(200)) togetherWith
|
||||
fadeOut(animationSpec = tween(150))
|
||||
}
|
||||
|
||||
// Default - мгновенный переход
|
||||
else -> {
|
||||
EnterTransition.None togetherWith ExitTransition.None
|
||||
}
|
||||
}
|
||||
|
||||
// 🔙 Выход из чата - плавный fade
|
||||
isExitingChat -> {
|
||||
fadeIn(
|
||||
animationSpec = tween(200)
|
||||
) togetherWith fadeOut(
|
||||
animationSpec = tween(150)
|
||||
)
|
||||
}
|
||||
|
||||
// 🔍 Вход в Search - плавный fade
|
||||
isEnteringSearch -> {
|
||||
fadeIn(
|
||||
animationSpec = tween(200)
|
||||
) togetherWith fadeOut(
|
||||
animationSpec = tween(150)
|
||||
)
|
||||
}
|
||||
|
||||
// 🔙 Выход из Search - плавный fade
|
||||
isEnteringSearch -> {
|
||||
fadeIn(
|
||||
animationSpec = tween(200)
|
||||
) togetherWith fadeOut(
|
||||
animationSpec = tween(150)
|
||||
)
|
||||
}
|
||||
|
||||
// 🔙 Выход из Search - плавный fade
|
||||
isExitingSearch -> {
|
||||
fadeIn(
|
||||
animationSpec = tween(200)
|
||||
) togetherWith fadeOut(
|
||||
animationSpec = tween(150)
|
||||
)
|
||||
}
|
||||
|
||||
// Default - мгновенный переход
|
||||
else -> {
|
||||
EnterTransition.None togetherWith ExitTransition.None
|
||||
}
|
||||
}
|
||||
},
|
||||
label = "screenNavigation"
|
||||
},
|
||||
label = "screenNavigation"
|
||||
) { (currentUser, isSearchOpen, _) ->
|
||||
when {
|
||||
currentUser != null -> {
|
||||
// Экран чата
|
||||
ChatDetailScreen(
|
||||
user = currentUser,
|
||||
currentUserPublicKey = accountPublicKey,
|
||||
currentUserPrivateKey = accountPrivateKey,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onBack = { selectedUser = null },
|
||||
onNavigateToChat = { publicKey ->
|
||||
// 📨 Forward: переход в выбранный чат
|
||||
// Нужно получить SearchUser из публичного ключа
|
||||
// Используем минимальные данные - остальное подгрузится в ChatDetailScreen
|
||||
selectedUser = SearchUser(
|
||||
title = "",
|
||||
username = "",
|
||||
publicKey = publicKey,
|
||||
verified = 0,
|
||||
online = 0
|
||||
)
|
||||
}
|
||||
user = currentUser,
|
||||
currentUserPublicKey = accountPublicKey,
|
||||
currentUserPrivateKey = accountPrivateKey,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onBack = { selectedUser = null },
|
||||
onNavigateToChat = { publicKey ->
|
||||
// 📨 Forward: переход в выбранный чат
|
||||
// Нужно получить SearchUser из публичного ключа
|
||||
// Используем минимальные данные - остальное подгрузится в
|
||||
// ChatDetailScreen
|
||||
selectedUser =
|
||||
SearchUser(
|
||||
title = "",
|
||||
username = "",
|
||||
publicKey = publicKey,
|
||||
verified = 0,
|
||||
online = 0
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
isSearchOpen -> {
|
||||
// Экран поиска
|
||||
SearchScreen(
|
||||
privateKeyHash = privateKeyHash,
|
||||
currentUserPublicKey = accountPublicKey,
|
||||
isDarkTheme = isDarkTheme,
|
||||
protocolState = protocolState,
|
||||
onBackClick = { showSearchScreen = false },
|
||||
onUserSelect = { selectedSearchUser ->
|
||||
showSearchScreen = false
|
||||
selectedUser = selectedSearchUser
|
||||
}
|
||||
privateKeyHash = privateKeyHash,
|
||||
currentUserPublicKey = accountPublicKey,
|
||||
isDarkTheme = isDarkTheme,
|
||||
protocolState = protocolState,
|
||||
onBackClick = { showSearchScreen = false },
|
||||
onUserSelect = { selectedSearchUser ->
|
||||
showSearchScreen = false
|
||||
selectedUser = selectedSearchUser
|
||||
}
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// Список чатов
|
||||
ChatsListScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
accountName = accountName,
|
||||
accountPhone = accountPhone,
|
||||
accountPublicKey = accountPublicKey,
|
||||
accountPrivateKey = accountPrivateKey,
|
||||
privateKeyHash = privateKeyHash,
|
||||
onToggleTheme = onToggleTheme,
|
||||
onProfileClick = {
|
||||
// TODO: Navigate to profile
|
||||
},
|
||||
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 = {
|
||||
// TODO: Navigate to settings
|
||||
},
|
||||
onInviteFriendsClick = {
|
||||
// TODO: Share invite link
|
||||
},
|
||||
onSearchClick = {
|
||||
showSearchScreen = true
|
||||
},
|
||||
onNewChat = {
|
||||
// TODO: Show new chat screen
|
||||
},
|
||||
onUserSelect = { selectedChatUser ->
|
||||
selectedUser = selectedChatUser
|
||||
},
|
||||
onLogout = onLogout
|
||||
isDarkTheme = isDarkTheme,
|
||||
accountName = accountName,
|
||||
accountPhone = accountPhone,
|
||||
accountPublicKey = accountPublicKey,
|
||||
accountPrivateKey = accountPrivateKey,
|
||||
privateKeyHash = privateKeyHash,
|
||||
onToggleTheme = onToggleTheme,
|
||||
onProfileClick = {
|
||||
// TODO: Navigate to profile
|
||||
},
|
||||
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 = {
|
||||
// TODO: Navigate to settings
|
||||
},
|
||||
onInviteFriendsClick = {
|
||||
// TODO: Share invite link
|
||||
},
|
||||
onSearchClick = { showSearchScreen = true },
|
||||
onNewChat = {
|
||||
// TODO: Show new chat screen
|
||||
},
|
||||
onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser },
|
||||
onLogout = onLogout
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,70 +6,59 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import com.rosetta.messenger.MainActivity
|
||||
import com.rosetta.messenger.R
|
||||
import com.rosetta.messenger.crypto.CryptoManager
|
||||
import com.rosetta.messenger.data.AccountManager
|
||||
import com.rosetta.messenger.network.ProtocolManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Firebase Cloud Messaging Service для обработки push-уведомлений
|
||||
*
|
||||
*
|
||||
* Обрабатывает:
|
||||
* - Получение нового FCM токена
|
||||
* - Получение push-уведомлений о новых сообщениях
|
||||
* - Отображение уведомлений
|
||||
*/
|
||||
class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
||||
|
||||
|
||||
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TAG = "RosettaFCM"
|
||||
private const val CHANNEL_ID = "rosetta_messages"
|
||||
private const val CHANNEL_NAME = "Messages"
|
||||
private const val NOTIFICATION_ID = 1
|
||||
|
||||
|
||||
// 🔥 Флаг - приложение в foreground (видимо пользователю)
|
||||
@Volatile
|
||||
var isAppInForeground = false
|
||||
@Volatile var isAppInForeground = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Вызывается когда получен новый FCM токен
|
||||
* Отправляем его на сервер через протокол
|
||||
*/
|
||||
|
||||
/** Вызывается когда получен новый FCM токен Отправляем его на сервер через протокол */
|
||||
override fun onNewToken(token: String) {
|
||||
super.onNewToken(token)
|
||||
|
||||
|
||||
// Сохраняем токен локально
|
||||
saveFcmToken(token)
|
||||
|
||||
|
||||
// 📤 Токен будет отправлен на сервер после успешного логина в MainActivity
|
||||
}
|
||||
|
||||
/**
|
||||
* Вызывается когда получено push-уведомление
|
||||
*/
|
||||
|
||||
/** Вызывается когда получено push-уведомление */
|
||||
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||
super.onMessageReceived(remoteMessage)
|
||||
|
||||
|
||||
// Обрабатываем data payload
|
||||
remoteMessage.data.isNotEmpty().let {
|
||||
|
||||
val type = remoteMessage.data["type"]
|
||||
val senderPublicKey = remoteMessage.data["sender_public_key"]
|
||||
val senderName = remoteMessage.data["sender_name"] ?: senderPublicKey?.take(10) ?: "Unknown"
|
||||
val senderName =
|
||||
remoteMessage.data["sender_name"] ?: senderPublicKey?.take(10) ?: "Unknown"
|
||||
val messagePreview = remoteMessage.data["message_preview"] ?: "New message"
|
||||
|
||||
|
||||
when (type) {
|
||||
"new_message" -> {
|
||||
// Показываем уведомление о новом сообщении
|
||||
@@ -78,112 +67,118 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
||||
"message_read" -> {
|
||||
// Сообщение прочитано - можно обновить UI если приложение открыто
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Обрабатываем notification payload (если есть)
|
||||
remoteMessage.notification?.let {
|
||||
showSimpleNotification(it.title ?: "Rosetta", it.body ?: "New message")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать уведомление о новом сообщении
|
||||
*/
|
||||
private fun showMessageNotification(senderPublicKey: String?, senderName: String, messagePreview: String) {
|
||||
|
||||
/** Показать уведомление о новом сообщении */
|
||||
private fun showMessageNotification(
|
||||
senderPublicKey: String?,
|
||||
senderName: String,
|
||||
messagePreview: String
|
||||
) {
|
||||
// 🔥 Не показываем уведомление если приложение открыто
|
||||
if (isAppInForeground) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
createNotificationChannel()
|
||||
|
||||
|
||||
// Intent для открытия чата
|
||||
val intent = Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
putExtra("open_chat", senderPublicKey)
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentTitle(senderName)
|
||||
.setContentText(messagePreview)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
|
||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val intent =
|
||||
Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
putExtra("open_chat", senderPublicKey)
|
||||
}
|
||||
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification =
|
||||
NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(senderName)
|
||||
.setContentText(messagePreview)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
|
||||
val notificationManager =
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать простое уведомление
|
||||
*/
|
||||
|
||||
/** Показать простое уведомление */
|
||||
private fun showSimpleNotification(title: String, body: String) {
|
||||
// 🔥 Не показываем уведомление если приложение открыто
|
||||
if (isAppInForeground) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
createNotificationChannel()
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentTitle(title)
|
||||
.setContentText(body)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
|
||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
val intent =
|
||||
Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
val pendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification =
|
||||
NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(title)
|
||||
.setContentText(body)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
|
||||
val notificationManager =
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать notification channel для Android 8+
|
||||
*/
|
||||
|
||||
/** Создать notification channel для Android 8+ */
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = "Notifications for new messages"
|
||||
enableVibration(true)
|
||||
}
|
||||
|
||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
.apply {
|
||||
description = "Notifications for new messages"
|
||||
enableVibration(true)
|
||||
}
|
||||
|
||||
val notificationManager =
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить FCM токен в SharedPreferences
|
||||
*/
|
||||
|
||||
/** Сохранить FCM токен в SharedPreferences */
|
||||
private fun saveFcmToken(token: String) {
|
||||
val prefs = getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE)
|
||||
prefs.edit().putString("fcm_token", token).apply()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user