Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
package com.rosetta.messenger
|
package com.rosetta.messenger
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -11,110 +10,115 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.compose.animation.*
|
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.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
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.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.Surface
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import com.google.firebase.FirebaseApp
|
import com.google.firebase.FirebaseApp
|
||||||
import com.google.firebase.messaging.FirebaseMessaging
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import com.rosetta.messenger.crypto.CryptoManager
|
|
||||||
import com.rosetta.messenger.data.AccountManager
|
import com.rosetta.messenger.data.AccountManager
|
||||||
import com.rosetta.messenger.data.DecryptedAccount
|
import com.rosetta.messenger.data.DecryptedAccount
|
||||||
import com.rosetta.messenger.data.PreferencesManager
|
import com.rosetta.messenger.data.PreferencesManager
|
||||||
import com.rosetta.messenger.data.RecentSearchesManager
|
import com.rosetta.messenger.data.RecentSearchesManager
|
||||||
import com.rosetta.messenger.network.PacketPushNotification
|
import com.rosetta.messenger.network.PacketPushNotification
|
||||||
import com.rosetta.messenger.network.PushNotificationAction
|
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
import com.rosetta.messenger.network.ProtocolManager
|
||||||
import com.rosetta.messenger.network.ProtocolState
|
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.AccountInfo
|
||||||
import com.rosetta.messenger.ui.auth.AuthFlow
|
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.ChatDetailScreen
|
||||||
|
import com.rosetta.messenger.ui.chats.ChatsListScreen
|
||||||
import com.rosetta.messenger.ui.chats.SearchScreen
|
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.components.OptimizedEmojiCache
|
||||||
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
|
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
|
||||||
import com.rosetta.messenger.ui.splash.SplashScreen
|
import com.rosetta.messenger.ui.splash.SplashScreen
|
||||||
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
|
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.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private lateinit var preferencesManager: PreferencesManager
|
private lateinit var preferencesManager: PreferencesManager
|
||||||
private lateinit var accountManager: AccountManager
|
private lateinit var accountManager: AccountManager
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MainActivity"
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
preferencesManager = PreferencesManager(this)
|
preferencesManager = PreferencesManager(this)
|
||||||
accountManager = AccountManager(this)
|
accountManager = AccountManager(this)
|
||||||
RecentSearchesManager.init(this)
|
RecentSearchesManager.init(this)
|
||||||
|
|
||||||
// 🔥 Инициализируем ProtocolManager для обработки онлайн статусов
|
// 🔥 Инициализируем ProtocolManager для обработки онлайн статусов
|
||||||
ProtocolManager.initialize(this)
|
ProtocolManager.initialize(this)
|
||||||
|
|
||||||
// 🔔 Инициализируем Firebase для push-уведомлений
|
// 🔔 Инициализируем Firebase для push-уведомлений
|
||||||
initializeFirebase()
|
initializeFirebase()
|
||||||
|
|
||||||
// 🔥 Помечаем что приложение в foreground
|
// 🔥 Помечаем что приложение в foreground
|
||||||
com.rosetta.messenger.push.RosettaFirebaseMessagingService.isAppInForeground = true
|
com.rosetta.messenger.push.RosettaFirebaseMessagingService.isAppInForeground = true
|
||||||
|
|
||||||
// 📱 Предзагружаем эмодзи в фоне для мгновенного открытия пикера
|
// 📱 Предзагружаем эмодзи в фоне для мгновенного открытия пикера
|
||||||
// Используем новый оптимизированный кэш
|
// Используем новый оптимизированный кэш
|
||||||
OptimizedEmojiCache.preload(this)
|
OptimizedEmojiCache.preload(this)
|
||||||
|
|
||||||
setContent { // 🔔 Запрос разрешения на уведомления для Android 13+
|
setContent { // 🔔 Запрос разрешения на уведомления для Android 13+
|
||||||
val notificationPermissionLauncher = rememberLauncherForActivityResult(
|
val notificationPermissionLauncher =
|
||||||
contract = ActivityResultContracts.RequestPermission(),
|
rememberLauncherForActivityResult(
|
||||||
onResult = { isGranted ->
|
contract = ActivityResultContracts.RequestPermission(),
|
||||||
}
|
onResult = { isGranted -> }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Запрашиваем разрешение при первом запуске (Android 13+)
|
// Запрашиваем разрешение при первом запуске (Android 13+)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
val hasPermission = ContextCompat.checkSelfPermission(
|
val hasPermission =
|
||||||
this@MainActivity,
|
ContextCompat.checkSelfPermission(
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
this@MainActivity,
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
notificationPermissionLauncher.launch(
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val isDarkTheme by preferencesManager.isDarkTheme.collectAsState(initial = true)
|
val isDarkTheme by preferencesManager.isDarkTheme.collectAsState(initial = true)
|
||||||
val isLoggedIn by accountManager.isLoggedIn.collectAsState(initial = null)
|
val isLoggedIn by accountManager.isLoggedIn.collectAsState(initial = null)
|
||||||
@@ -123,152 +127,179 @@ class MainActivity : ComponentActivity() {
|
|||||||
var hasExistingAccount by remember { mutableStateOf<Boolean?>(null) }
|
var hasExistingAccount by remember { mutableStateOf<Boolean?>(null) }
|
||||||
var currentAccount by remember { mutableStateOf<DecryptedAccount?>(null) }
|
var currentAccount by remember { mutableStateOf<DecryptedAccount?>(null) }
|
||||||
var accountInfoList by remember { mutableStateOf<List<AccountInfo>>(emptyList()) }
|
var accountInfoList by remember { mutableStateOf<List<AccountInfo>>(emptyList()) }
|
||||||
|
|
||||||
// Check for existing accounts and build AccountInfo list
|
// Check for existing accounts and build AccountInfo list
|
||||||
// Also force logout so user always sees unlock screen on app restart
|
// Also force logout so user always sees unlock screen on app restart
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
accountManager.logout() // Always start logged out
|
accountManager.logout() // Always start logged out
|
||||||
val accounts = accountManager.getAllAccounts()
|
val accounts = accountManager.getAllAccounts()
|
||||||
hasExistingAccount = accounts.isNotEmpty()
|
hasExistingAccount = accounts.isNotEmpty()
|
||||||
accountInfoList = accounts.map { account ->
|
accountInfoList =
|
||||||
val shortKey = account.publicKey.take(7)
|
accounts.map { account ->
|
||||||
val displayName = account.name ?: shortKey
|
val shortKey = account.publicKey.take(7)
|
||||||
val initials = displayName.trim().split(Regex("\\s+"))
|
val displayName = account.name ?: shortKey
|
||||||
.filter { it.isNotEmpty() }
|
val initials =
|
||||||
.let { words ->
|
displayName
|
||||||
when {
|
.trim()
|
||||||
words.isEmpty() -> "??"
|
.split(Regex("\\s+"))
|
||||||
words.size == 1 -> words[0].take(2).uppercase()
|
.filter { it.isNotEmpty() }
|
||||||
else -> "${words[0].first()}${words[1].first()}".uppercase()
|
.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
|
// Wait for initial load
|
||||||
if (hasExistingAccount == null) {
|
if (hasExistingAccount == null) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier.fillMaxSize()
|
||||||
.background(if (isDarkTheme) Color(0xFF1B1B1B) else Color.White)
|
.background(
|
||||||
|
if (isDarkTheme) Color(0xFF1B1B1B) else Color.White
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return@setContent
|
return@setContent
|
||||||
}
|
}
|
||||||
|
|
||||||
RosettaAndroidTheme(
|
RosettaAndroidTheme(darkTheme = isDarkTheme, animated = true) {
|
||||||
darkTheme = isDarkTheme,
|
|
||||||
animated = true
|
|
||||||
) {
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = if (isDarkTheme) Color(0xFF1B1B1B) else Color.White
|
color = if (isDarkTheme) Color(0xFF1B1B1B) else Color.White
|
||||||
) {
|
) {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = when {
|
targetState =
|
||||||
showSplash -> "splash"
|
when {
|
||||||
showOnboarding && hasExistingAccount == false -> "onboarding"
|
showSplash -> "splash"
|
||||||
isLoggedIn != true && hasExistingAccount == false -> "auth_new"
|
showOnboarding && hasExistingAccount == false ->
|
||||||
isLoggedIn != true && hasExistingAccount == true -> "auth_unlock"
|
"onboarding"
|
||||||
else -> "main"
|
isLoggedIn != true && hasExistingAccount == false ->
|
||||||
},
|
"auth_new"
|
||||||
transitionSpec = {
|
isLoggedIn != true && hasExistingAccount == true ->
|
||||||
fadeIn(animationSpec = tween(600)) togetherWith
|
"auth_unlock"
|
||||||
fadeOut(animationSpec = tween(600))
|
else -> "main"
|
||||||
},
|
},
|
||||||
label = "screenTransition"
|
transitionSpec = {
|
||||||
|
fadeIn(animationSpec = tween(600)) togetherWith
|
||||||
|
fadeOut(animationSpec = tween(600))
|
||||||
|
},
|
||||||
|
label = "screenTransition"
|
||||||
) { screen ->
|
) { screen ->
|
||||||
when (screen) {
|
when (screen) {
|
||||||
"splash" -> {
|
"splash" -> {
|
||||||
SplashScreen(
|
SplashScreen(
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
onSplashComplete = { showSplash = false }
|
onSplashComplete = { showSplash = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"onboarding" -> {
|
"onboarding" -> {
|
||||||
OnboardingScreen(
|
OnboardingScreen(
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
onThemeToggle = {
|
onThemeToggle = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
preferencesManager.setDarkTheme(!isDarkTheme)
|
preferencesManager.setDarkTheme(!isDarkTheme)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStartMessaging = {
|
onStartMessaging = { showOnboarding = false }
|
||||||
showOnboarding = false
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"auth_new", "auth_unlock" -> {
|
"auth_new", "auth_unlock" -> {
|
||||||
AuthFlow(
|
AuthFlow(
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
hasExistingAccount = screen == "auth_unlock",
|
hasExistingAccount = screen == "auth_unlock",
|
||||||
accounts = accountInfoList,
|
accounts = accountInfoList,
|
||||||
accountManager = accountManager,
|
accountManager = accountManager,
|
||||||
onAuthComplete = { account ->
|
onAuthComplete = { account ->
|
||||||
currentAccount = account
|
currentAccount = account
|
||||||
hasExistingAccount = true
|
hasExistingAccount = true
|
||||||
// Save as last logged account
|
// Save as last logged account
|
||||||
account?.let { accountManager.setLastLoggedPublicKey(it.publicKey) }
|
account?.let {
|
||||||
|
accountManager.setLastLoggedPublicKey(it.publicKey)
|
||||||
// 📤 Отправляем FCM токен на сервер после успешной аутентификации
|
}
|
||||||
account?.let { sendFcmTokenToServer(it) }
|
|
||||||
|
// 📤 Отправляем FCM токен на сервер после успешной
|
||||||
// Reload accounts list
|
// аутентификации
|
||||||
scope.launch {
|
account?.let { sendFcmTokenToServer(it) }
|
||||||
val accounts = accountManager.getAllAccounts()
|
|
||||||
accountInfoList = accounts.map { acc ->
|
// Reload accounts list
|
||||||
val shortKey = acc.publicKey.take(7)
|
scope.launch {
|
||||||
val displayName = acc.name ?: shortKey
|
val accounts = accountManager.getAllAccounts()
|
||||||
val initials = displayName.trim().split(Regex("\\s+"))
|
accountInfoList =
|
||||||
.filter { it.isNotEmpty() }
|
accounts.map { acc ->
|
||||||
.let { words ->
|
val shortKey = acc.publicKey.take(7)
|
||||||
when {
|
val displayName = acc.name ?: shortKey
|
||||||
words.isEmpty() -> "??"
|
val initials =
|
||||||
words.size == 1 -> words[0].take(2).uppercase()
|
displayName
|
||||||
else -> "${words[0].first()}${words[1].first()}".uppercase()
|
.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,
|
onLogout = {
|
||||||
name = displayName,
|
// Set currentAccount to null immediately to prevent UI
|
||||||
initials = initials,
|
// lag
|
||||||
publicKey = acc.publicKey
|
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" -> {
|
"main" -> {
|
||||||
MainScreen(
|
MainScreen(
|
||||||
account = currentAccount,
|
account = currentAccount,
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
onToggleTheme = {
|
onToggleTheme = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
preferencesManager.setDarkTheme(!isDarkTheme)
|
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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
// 🔥 Приложение стало видимым - отключаем уведомления
|
// 🔥 Приложение стало видимым - отключаем уведомления
|
||||||
com.rosetta.messenger.push.RosettaFirebaseMessagingService.isAppInForeground = true
|
com.rosetta.messenger.push.RosettaFirebaseMessagingService.isAppInForeground = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
// 🔥 Приложение ушло в background - включаем уведомления
|
// 🔥 Приложение ушло в background - включаем уведомления
|
||||||
com.rosetta.messenger.push.RosettaFirebaseMessagingService.isAppInForeground = false
|
com.rosetta.messenger.push.RosettaFirebaseMessagingService.isAppInForeground = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 🔔 Инициализация Firebase Cloud Messaging */
|
||||||
* 🔔 Инициализация Firebase Cloud Messaging
|
|
||||||
*/
|
|
||||||
private fun initializeFirebase() {
|
private fun initializeFirebase() {
|
||||||
try {
|
try {
|
||||||
|
addFcmLog("🔔 Инициализация Firebase...")
|
||||||
// Инициализируем Firebase
|
// Инициализируем Firebase
|
||||||
FirebaseApp.initializeApp(this)
|
FirebaseApp.initializeApp(this)
|
||||||
|
addFcmLog("✅ Firebase инициализирован")
|
||||||
|
|
||||||
// Получаем FCM токен
|
// Получаем FCM токен
|
||||||
|
addFcmLog("📲 Запрос FCM токена...")
|
||||||
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
|
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
|
||||||
if (!task.isSuccessful) {
|
if (!task.isSuccessful) {
|
||||||
|
addFcmLog("❌ Ошибка получения токена: ${task.exception?.message}")
|
||||||
return@addOnCompleteListener
|
return@addOnCompleteListener
|
||||||
}
|
}
|
||||||
|
|
||||||
val token = task.result
|
val token = task.result
|
||||||
|
|
||||||
// Сохраняем токен локально
|
if (token != null) {
|
||||||
token?.let { saveFcmToken(it) }
|
val shortToken = "${token.take(12)}...${token.takeLast(8)}"
|
||||||
|
addFcmLog("✅ FCM токен получен: $shortToken")
|
||||||
|
// Сохраняем токен локально
|
||||||
|
saveFcmToken(token)
|
||||||
|
addFcmLog("💾 Токен сохранен локально")
|
||||||
|
} else {
|
||||||
|
addFcmLog("⚠️ Токен пустой")
|
||||||
|
}
|
||||||
|
|
||||||
// Токен будет отправлен на сервер после успешной аутентификации
|
// Токен будет отправлен на сервер после успешной аутентификации
|
||||||
// (см. вызов sendFcmTokenToServer в onAccountLogin)
|
// (см. вызов sendFcmTokenToServer в onAccountLogin)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
addFcmLog("❌ Ошибка Firebase: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Сохранить FCM токен в SharedPreferences */
|
||||||
* Сохранить FCM токен в SharedPreferences
|
|
||||||
*/
|
|
||||||
private fun saveFcmToken(token: String) {
|
private fun saveFcmToken(token: String) {
|
||||||
val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE)
|
val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE)
|
||||||
prefs.edit().putString("fcm_token", token).apply()
|
prefs.edit().putString("fcm_token", token).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Отправить FCM токен на сервер
|
* Отправить FCM токен на сервер Вызывается после успешной аутентификации, когда аккаунт уже
|
||||||
* Вызывается после успешной аутентификации, когда аккаунт уже расшифрован
|
* расшифрован
|
||||||
*/
|
*/
|
||||||
private fun sendFcmTokenToServer(account: DecryptedAccount) {
|
private fun sendFcmTokenToServer(account: DecryptedAccount) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE)
|
val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE)
|
||||||
val token = prefs.getString("fcm_token", null)
|
val token = prefs.getString("fcm_token", null)
|
||||||
|
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
|
addFcmLog("⚠️ Нет сохраненного токена для отправки")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val shortToken = "${token.take(12)}...${token.takeLast(8)}"
|
||||||
|
addFcmLog("📤 Подготовка к отправке токена на сервер")
|
||||||
|
addFcmLog("⏳ Ожидание аутентификации...")
|
||||||
|
|
||||||
// 🔥 КРИТИЧНО: Ждем пока протокол станет AUTHENTICATED
|
// 🔥 КРИТИЧНО: Ждем пока протокол станет AUTHENTICATED
|
||||||
var waitAttempts = 0
|
var waitAttempts = 0
|
||||||
while (ProtocolManager.state.value != ProtocolState.AUTHENTICATED && waitAttempts < 50) {
|
while (ProtocolManager.state.value != ProtocolState.AUTHENTICATED &&
|
||||||
|
waitAttempts < 50) {
|
||||||
delay(100) // Ждем 100ms
|
delay(100) // Ждем 100ms
|
||||||
waitAttempts++
|
waitAttempts++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ProtocolManager.state.value != ProtocolState.AUTHENTICATED) {
|
if (ProtocolManager.state.value != ProtocolState.AUTHENTICATED) {
|
||||||
|
addFcmLog("❌ Таймаут аутентификации (${waitAttempts * 100}ms)")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val packet = PacketPushNotification().apply {
|
addFcmLog("✅ Аутентификация успешна")
|
||||||
this.notificationsToken = token
|
addFcmLog("📨 Отправка токена: $shortToken")
|
||||||
this.action = PushNotificationAction.SUBSCRIBE
|
|
||||||
}
|
val packet =
|
||||||
|
PacketPushNotification().apply {
|
||||||
|
this.notificationsToken = token
|
||||||
|
this.action = PushNotificationAction.SUBSCRIBE
|
||||||
|
}
|
||||||
|
|
||||||
ProtocolManager.send(packet)
|
ProtocolManager.send(packet)
|
||||||
|
addFcmLog("✅ Пакет отправлен на сервер (ID: 0x10)")
|
||||||
|
addFcmLog("🎉 FCM токен успешно зарегистрирован!")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
addFcmLog("❌ Ошибка отправки: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,177 +416,162 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
account: DecryptedAccount? = null,
|
account: DecryptedAccount? = null,
|
||||||
isDarkTheme: Boolean = true,
|
isDarkTheme: Boolean = true,
|
||||||
onToggleTheme: () -> Unit = {},
|
onToggleTheme: () -> Unit = {},
|
||||||
onLogout: () -> Unit = {}
|
onLogout: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val accountName = account?.name ?: "Account"
|
val accountName = account?.name ?: "Account"
|
||||||
val accountPhone = account?.publicKey?.take(16)?.let {
|
val accountPhone =
|
||||||
"+${it.take(1)} ${it.substring(1, 4)} ${it.substring(4, 7)}${it.substring(7)}"
|
account?.publicKey?.take(16)?.let {
|
||||||
} ?: "+7 775 9932587"
|
"+${it.take(1)} ${it.substring(1, 4)} ${it.substring(4, 7)}${it.substring(7)}"
|
||||||
|
}
|
||||||
|
?: "+7 775 9932587"
|
||||||
val accountPublicKey = account?.publicKey ?: "04c266b98ae5"
|
val accountPublicKey = account?.publicKey ?: "04c266b98ae5"
|
||||||
val accountPrivateKey = account?.privateKey ?: ""
|
val accountPrivateKey = account?.privateKey ?: ""
|
||||||
val privateKeyHash = account?.privateKeyHash ?: ""
|
val privateKeyHash = account?.privateKeyHash ?: ""
|
||||||
|
|
||||||
// Состояние протокола для передачи в SearchScreen
|
// Состояние протокола для передачи в SearchScreen
|
||||||
val protocolState by ProtocolManager.state.collectAsState()
|
val protocolState by ProtocolManager.state.collectAsState()
|
||||||
|
|
||||||
// Навигация между экранами
|
// Навигация между экранами
|
||||||
var selectedUser by remember { mutableStateOf<SearchUser?>(null) }
|
var selectedUser by remember { mutableStateOf<SearchUser?>(null) }
|
||||||
var showSearchScreen by remember { mutableStateOf(false) }
|
var showSearchScreen by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// 🔥 TELEGRAM-STYLE анимация - чистый slide БЕЗ прозрачности
|
// 🔥 TELEGRAM-STYLE анимация - чистый slide БЕЗ прозрачности
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = Triple(selectedUser, showSearchScreen, Unit),
|
targetState = Triple(selectedUser, showSearchScreen, Unit),
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
val isEnteringChat = targetState.first != null && initialState.first == null
|
val isEnteringChat = targetState.first != null && initialState.first == null
|
||||||
val isExitingChat = targetState.first == null && initialState.first != null
|
val isExitingChat = targetState.first == null && initialState.first != null
|
||||||
val isEnteringSearch = targetState.second && !initialState.second
|
val isEnteringSearch = targetState.second && !initialState.second
|
||||||
val isExitingSearch = !targetState.second && initialState.second
|
val isExitingSearch = !targetState.second && initialState.second
|
||||||
|
|
||||||
when {
|
when {
|
||||||
// 🚀 Вход в чат - плавный fade
|
// 🚀 Вход в чат - плавный fade
|
||||||
isEnteringChat -> {
|
isEnteringChat -> {
|
||||||
fadeIn(
|
fadeIn(animationSpec = tween(200)) togetherWith
|
||||||
animationSpec = tween(200)
|
fadeOut(animationSpec = tween(150))
|
||||||
) 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
|
label = "screenNavigation"
|
||||||
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"
|
|
||||||
) { (currentUser, isSearchOpen, _) ->
|
) { (currentUser, isSearchOpen, _) ->
|
||||||
when {
|
when {
|
||||||
currentUser != null -> {
|
currentUser != null -> {
|
||||||
// Экран чата
|
// Экран чата
|
||||||
ChatDetailScreen(
|
ChatDetailScreen(
|
||||||
user = currentUser,
|
user = currentUser,
|
||||||
currentUserPublicKey = accountPublicKey,
|
currentUserPublicKey = accountPublicKey,
|
||||||
currentUserPrivateKey = accountPrivateKey,
|
currentUserPrivateKey = accountPrivateKey,
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
onBack = { selectedUser = null },
|
onBack = { selectedUser = null },
|
||||||
onNavigateToChat = { publicKey ->
|
onNavigateToChat = { publicKey ->
|
||||||
// 📨 Forward: переход в выбранный чат
|
// 📨 Forward: переход в выбранный чат
|
||||||
// Нужно получить SearchUser из публичного ключа
|
// Нужно получить SearchUser из публичного ключа
|
||||||
// Используем минимальные данные - остальное подгрузится в ChatDetailScreen
|
// Используем минимальные данные - остальное подгрузится в
|
||||||
selectedUser = SearchUser(
|
// ChatDetailScreen
|
||||||
title = "",
|
selectedUser =
|
||||||
username = "",
|
SearchUser(
|
||||||
publicKey = publicKey,
|
title = "",
|
||||||
verified = 0,
|
username = "",
|
||||||
online = 0
|
publicKey = publicKey,
|
||||||
)
|
verified = 0,
|
||||||
}
|
online = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
isSearchOpen -> {
|
isSearchOpen -> {
|
||||||
// Экран поиска
|
// Экран поиска
|
||||||
SearchScreen(
|
SearchScreen(
|
||||||
privateKeyHash = privateKeyHash,
|
privateKeyHash = privateKeyHash,
|
||||||
currentUserPublicKey = accountPublicKey,
|
currentUserPublicKey = accountPublicKey,
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
protocolState = protocolState,
|
protocolState = protocolState,
|
||||||
onBackClick = { showSearchScreen = false },
|
onBackClick = { showSearchScreen = false },
|
||||||
onUserSelect = { selectedSearchUser ->
|
onUserSelect = { selectedSearchUser ->
|
||||||
showSearchScreen = false
|
showSearchScreen = false
|
||||||
selectedUser = selectedSearchUser
|
selectedUser = selectedSearchUser
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Список чатов
|
// Список чатов
|
||||||
ChatsListScreen(
|
ChatsListScreen(
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
accountName = accountName,
|
accountName = accountName,
|
||||||
accountPhone = accountPhone,
|
accountPhone = accountPhone,
|
||||||
accountPublicKey = accountPublicKey,
|
accountPublicKey = accountPublicKey,
|
||||||
accountPrivateKey = accountPrivateKey,
|
accountPrivateKey = accountPrivateKey,
|
||||||
privateKeyHash = privateKeyHash,
|
privateKeyHash = privateKeyHash,
|
||||||
onToggleTheme = onToggleTheme,
|
onToggleTheme = onToggleTheme,
|
||||||
onProfileClick = {
|
onProfileClick = {
|
||||||
// TODO: Navigate to profile
|
// TODO: Navigate to profile
|
||||||
},
|
},
|
||||||
onNewGroupClick = {
|
onNewGroupClick = {
|
||||||
// TODO: Navigate to new group
|
// TODO: Navigate to new group
|
||||||
},
|
},
|
||||||
onContactsClick = {
|
onContactsClick = {
|
||||||
// TODO: Navigate to contacts
|
// TODO: Navigate to contacts
|
||||||
},
|
},
|
||||||
onCallsClick = {
|
onCallsClick = {
|
||||||
// TODO: Navigate to calls
|
// TODO: Navigate to calls
|
||||||
},
|
},
|
||||||
onSavedMessagesClick = {
|
onSavedMessagesClick = {
|
||||||
// Открываем чат с самим собой (Saved Messages)
|
// Открываем чат с самим собой (Saved Messages)
|
||||||
selectedUser = SearchUser(
|
selectedUser =
|
||||||
title = "Saved Messages",
|
SearchUser(
|
||||||
username = "",
|
title = "Saved Messages",
|
||||||
publicKey = accountPublicKey,
|
username = "",
|
||||||
verified = 0,
|
publicKey = accountPublicKey,
|
||||||
online = 1
|
verified = 0,
|
||||||
)
|
online = 1
|
||||||
},
|
)
|
||||||
onSettingsClick = {
|
},
|
||||||
// TODO: Navigate to settings
|
onSettingsClick = {
|
||||||
},
|
// TODO: Navigate to settings
|
||||||
onInviteFriendsClick = {
|
},
|
||||||
// TODO: Share invite link
|
onInviteFriendsClick = {
|
||||||
},
|
// TODO: Share invite link
|
||||||
onSearchClick = {
|
},
|
||||||
showSearchScreen = true
|
onSearchClick = { showSearchScreen = true },
|
||||||
},
|
onNewChat = {
|
||||||
onNewChat = {
|
// TODO: Show new chat screen
|
||||||
// TODO: Show new chat screen
|
},
|
||||||
},
|
onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser },
|
||||||
onUserSelect = { selectedChatUser ->
|
onLogout = onLogout
|
||||||
selectedUser = selectedChatUser
|
|
||||||
},
|
|
||||||
onLogout = onLogout
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,70 +6,59 @@ import android.app.PendingIntent
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
import com.google.firebase.messaging.RemoteMessage
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
import com.rosetta.messenger.MainActivity
|
import com.rosetta.messenger.MainActivity
|
||||||
import com.rosetta.messenger.R
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Firebase Cloud Messaging Service для обработки push-уведомлений
|
* Firebase Cloud Messaging Service для обработки push-уведомлений
|
||||||
*
|
*
|
||||||
* Обрабатывает:
|
* Обрабатывает:
|
||||||
* - Получение нового FCM токена
|
* - Получение нового FCM токена
|
||||||
* - Получение push-уведомлений о новых сообщениях
|
* - Получение push-уведомлений о новых сообщениях
|
||||||
* - Отображение уведомлений
|
* - Отображение уведомлений
|
||||||
*/
|
*/
|
||||||
class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
|
|
||||||
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "RosettaFCM"
|
private const val TAG = "RosettaFCM"
|
||||||
private const val CHANNEL_ID = "rosetta_messages"
|
private const val CHANNEL_ID = "rosetta_messages"
|
||||||
private const val CHANNEL_NAME = "Messages"
|
private const val CHANNEL_NAME = "Messages"
|
||||||
private const val NOTIFICATION_ID = 1
|
private const val NOTIFICATION_ID = 1
|
||||||
|
|
||||||
// 🔥 Флаг - приложение в foreground (видимо пользователю)
|
// 🔥 Флаг - приложение в foreground (видимо пользователю)
|
||||||
@Volatile
|
@Volatile var isAppInForeground = false
|
||||||
var isAppInForeground = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Вызывается когда получен новый FCM токен Отправляем его на сервер через протокол */
|
||||||
* Вызывается когда получен новый FCM токен
|
|
||||||
* Отправляем его на сервер через протокол
|
|
||||||
*/
|
|
||||||
override fun onNewToken(token: String) {
|
override fun onNewToken(token: String) {
|
||||||
super.onNewToken(token)
|
super.onNewToken(token)
|
||||||
|
|
||||||
// Сохраняем токен локально
|
// Сохраняем токен локально
|
||||||
saveFcmToken(token)
|
saveFcmToken(token)
|
||||||
|
|
||||||
// 📤 Токен будет отправлен на сервер после успешного логина в MainActivity
|
// 📤 Токен будет отправлен на сервер после успешного логина в MainActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Вызывается когда получено push-уведомление */
|
||||||
* Вызывается когда получено push-уведомление
|
|
||||||
*/
|
|
||||||
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||||
super.onMessageReceived(remoteMessage)
|
super.onMessageReceived(remoteMessage)
|
||||||
|
|
||||||
// Обрабатываем data payload
|
// Обрабатываем data payload
|
||||||
remoteMessage.data.isNotEmpty().let {
|
remoteMessage.data.isNotEmpty().let {
|
||||||
|
|
||||||
val type = remoteMessage.data["type"]
|
val type = remoteMessage.data["type"]
|
||||||
val senderPublicKey = remoteMessage.data["sender_public_key"]
|
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"
|
val messagePreview = remoteMessage.data["message_preview"] ?: "New message"
|
||||||
|
|
||||||
when (type) {
|
when (type) {
|
||||||
"new_message" -> {
|
"new_message" -> {
|
||||||
// Показываем уведомление о новом сообщении
|
// Показываем уведомление о новом сообщении
|
||||||
@@ -78,112 +67,118 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
"message_read" -> {
|
"message_read" -> {
|
||||||
// Сообщение прочитано - можно обновить UI если приложение открыто
|
// Сообщение прочитано - можно обновить UI если приложение открыто
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обрабатываем notification payload (если есть)
|
// Обрабатываем notification payload (если есть)
|
||||||
remoteMessage.notification?.let {
|
remoteMessage.notification?.let {
|
||||||
showSimpleNotification(it.title ?: "Rosetta", it.body ?: "New message")
|
showSimpleNotification(it.title ?: "Rosetta", it.body ?: "New message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Показать уведомление о новом сообщении */
|
||||||
* Показать уведомление о новом сообщении
|
private fun showMessageNotification(
|
||||||
*/
|
senderPublicKey: String?,
|
||||||
private fun showMessageNotification(senderPublicKey: String?, senderName: String, messagePreview: String) {
|
senderName: String,
|
||||||
|
messagePreview: String
|
||||||
|
) {
|
||||||
// 🔥 Не показываем уведомление если приложение открыто
|
// 🔥 Не показываем уведомление если приложение открыто
|
||||||
if (isAppInForeground) {
|
if (isAppInForeground) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
|
|
||||||
// Intent для открытия чата
|
// Intent для открытия чата
|
||||||
val intent = Intent(this, MainActivity::class.java).apply {
|
val intent =
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
Intent(this, MainActivity::class.java).apply {
|
||||||
putExtra("open_chat", senderPublicKey)
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
}
|
putExtra("open_chat", senderPublicKey)
|
||||||
|
}
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
|
||||||
this,
|
val pendingIntent =
|
||||||
0,
|
PendingIntent.getActivity(
|
||||||
intent,
|
this,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
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)
|
val notification =
|
||||||
.setContentText(messagePreview)
|
NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
.setContentTitle(senderName)
|
||||||
.setAutoCancel(true)
|
.setContentText(messagePreview)
|
||||||
.setContentIntent(pendingIntent)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.build()
|
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||||
|
.setAutoCancel(true)
|
||||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
.setContentIntent(pendingIntent)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val notificationManager =
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Показать простое уведомление */
|
||||||
* Показать простое уведомление
|
|
||||||
*/
|
|
||||||
private fun showSimpleNotification(title: String, body: String) {
|
private fun showSimpleNotification(title: String, body: String) {
|
||||||
// 🔥 Не показываем уведомление если приложение открыто
|
// 🔥 Не показываем уведомление если приложение открыто
|
||||||
if (isAppInForeground) {
|
if (isAppInForeground) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
|
|
||||||
val intent = Intent(this, MainActivity::class.java).apply {
|
val intent =
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
Intent(this, MainActivity::class.java).apply {
|
||||||
}
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
|
||||||
this,
|
val pendingIntent =
|
||||||
0,
|
PendingIntent.getActivity(
|
||||||
intent,
|
this,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
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)
|
val notification =
|
||||||
.setContentText(body)
|
NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setAutoCancel(true)
|
.setContentTitle(title)
|
||||||
.setContentIntent(pendingIntent)
|
.setContentText(body)
|
||||||
.build()
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setAutoCancel(true)
|
||||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
.setContentIntent(pendingIntent)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val notificationManager =
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Создать notification channel для Android 8+ */
|
||||||
* Создать notification channel для Android 8+
|
|
||||||
*/
|
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val channel = NotificationChannel(
|
val channel =
|
||||||
CHANNEL_ID,
|
NotificationChannel(
|
||||||
CHANNEL_NAME,
|
CHANNEL_ID,
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
CHANNEL_NAME,
|
||||||
).apply {
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
description = "Notifications for new messages"
|
)
|
||||||
enableVibration(true)
|
.apply {
|
||||||
}
|
description = "Notifications for new messages"
|
||||||
|
enableVibration(true)
|
||||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
}
|
||||||
|
|
||||||
|
val notificationManager =
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Сохранить FCM токен в SharedPreferences */
|
||||||
* Сохранить FCM токен в SharedPreferences
|
|
||||||
*/
|
|
||||||
private fun saveFcmToken(token: String) {
|
private fun saveFcmToken(token: String) {
|
||||||
val prefs = getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE)
|
val prefs = getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE)
|
||||||
prefs.edit().putString("fcm_token", token).apply()
|
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
9
app/src/main/res/drawable/ic_notification.xml
Normal file
9
app/src/main/res/drawable/ic_notification.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M20,2H4c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zM18,14H6v-2h12v2zM18,11H6V9h12v2zM18,8H6V6h12v2z"/>
|
||||||
|
</vector>
|
||||||
Reference in New Issue
Block a user