Архитектурный рефакторинг: единый SessionStore/SessionReducer, Hilt DI и декомпозиция ProtocolManager
This commit is contained in:
@@ -2,6 +2,7 @@ plugins {
|
|||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("kotlin-kapt")
|
id("kotlin-kapt")
|
||||||
|
id("com.google.dagger.hilt.android")
|
||||||
id("com.google.gms.google-services")
|
id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +120,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes = true
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.core:core-ktx:1.12.0")
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
||||||
@@ -182,6 +187,11 @@ dependencies {
|
|||||||
implementation("androidx.room:room-ktx:2.6.1")
|
implementation("androidx.room:room-ktx:2.6.1")
|
||||||
kapt("androidx.room:room-compiler:2.6.1")
|
kapt("androidx.room:room-compiler:2.6.1")
|
||||||
|
|
||||||
|
// Hilt DI
|
||||||
|
implementation("com.google.dagger:hilt-android:2.51.1")
|
||||||
|
kapt("com.google.dagger:hilt-compiler:2.51.1")
|
||||||
|
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
|
||||||
|
|
||||||
// Biometric authentication
|
// Biometric authentication
|
||||||
implementation("androidx.biometric:biometric:1.1.0")
|
implementation("androidx.biometric:biometric:1.1.0")
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,21 @@
|
|||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="https" android:host="rosetta.im" />
|
<data android:scheme="https" android:host="rosetta.im" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/*" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- App Icon Aliases: only one enabled at a time -->
|
<!-- App Icon Aliases: only one enabled at a time -->
|
||||||
|
|||||||
@@ -16,14 +16,19 @@ import com.rosetta.messenger.network.CallPhase
|
|||||||
import com.rosetta.messenger.repository.AvatarRepository
|
import com.rosetta.messenger.repository.AvatarRepository
|
||||||
import com.rosetta.messenger.ui.chats.calls.CallOverlay
|
import com.rosetta.messenger.ui.chats.calls.CallOverlay
|
||||||
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
|
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Лёгкая Activity для показа входящего звонка на lock screen.
|
* Лёгкая Activity для показа входящего звонка на lock screen.
|
||||||
* Показывается поверх экрана блокировки, без auth/splash.
|
* Показывается поверх экрана блокировки, без auth/splash.
|
||||||
* При Accept → переходит в MainActivity. При Decline → закрывается.
|
* При Accept → переходит в MainActivity. При Decline → закрывается.
|
||||||
*/
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
class IncomingCallActivity : ComponentActivity() {
|
class IncomingCallActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
@Inject lateinit var accountManager: AccountManager
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "IncomingCallActivity"
|
private const val TAG = "IncomingCallActivity"
|
||||||
}
|
}
|
||||||
@@ -119,7 +124,7 @@ class IncomingCallActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val avatarRepository = remember {
|
val avatarRepository = remember {
|
||||||
val accountKey = AccountManager(applicationContext).getLastLoggedPublicKey().orEmpty()
|
val accountKey = accountManager.getLastLoggedPublicKey().orEmpty()
|
||||||
if (accountKey.isNotBlank()) {
|
if (accountKey.isNotBlank()) {
|
||||||
val db = RosettaDatabase.getDatabase(applicationContext)
|
val db = RosettaDatabase.getDatabase(applicationContext)
|
||||||
AvatarRepository(
|
AvatarRepository(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.content.pm.PackageManager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.OpenableColumns
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -47,20 +48,24 @@ import com.rosetta.messenger.data.AccountManager
|
|||||||
import com.rosetta.messenger.data.DecryptedAccount
|
import com.rosetta.messenger.data.DecryptedAccount
|
||||||
import com.rosetta.messenger.data.EncryptedAccount
|
import com.rosetta.messenger.data.EncryptedAccount
|
||||||
import com.rosetta.messenger.data.PreferencesManager
|
import com.rosetta.messenger.data.PreferencesManager
|
||||||
|
import com.rosetta.messenger.data.MessageRepository
|
||||||
|
import com.rosetta.messenger.data.GroupRepository
|
||||||
import com.rosetta.messenger.crypto.CryptoManager
|
import com.rosetta.messenger.crypto.CryptoManager
|
||||||
import com.rosetta.messenger.data.RecentSearchesManager
|
import com.rosetta.messenger.data.RecentSearchesManager
|
||||||
import com.rosetta.messenger.data.isPlaceholderAccountName
|
import com.rosetta.messenger.data.isPlaceholderAccountName
|
||||||
import com.rosetta.messenger.data.resolveAccountDisplayName
|
import com.rosetta.messenger.data.resolveAccountDisplayName
|
||||||
import com.rosetta.messenger.database.RosettaDatabase
|
import com.rosetta.messenger.database.RosettaDatabase
|
||||||
|
import com.rosetta.messenger.di.IdentityGateway
|
||||||
|
import com.rosetta.messenger.di.ProtocolGateway
|
||||||
|
import com.rosetta.messenger.di.SessionCoordinator
|
||||||
import com.rosetta.messenger.network.CallActionResult
|
import com.rosetta.messenger.network.CallActionResult
|
||||||
import com.rosetta.messenger.network.CallPhase
|
import com.rosetta.messenger.network.CallPhase
|
||||||
import com.rosetta.messenger.network.CallManager
|
import com.rosetta.messenger.network.CallManager
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
import com.rosetta.messenger.network.AttachmentType
|
||||||
|
import com.rosetta.messenger.network.MessageAttachment
|
||||||
import com.rosetta.messenger.network.ProtocolState
|
import com.rosetta.messenger.network.ProtocolState
|
||||||
import com.rosetta.messenger.network.SearchUser
|
import com.rosetta.messenger.network.SearchUser
|
||||||
import com.rosetta.messenger.repository.AvatarRepository
|
import com.rosetta.messenger.repository.AvatarRepository
|
||||||
import com.rosetta.messenger.session.AppSessionCoordinator
|
|
||||||
import com.rosetta.messenger.session.IdentityStore
|
|
||||||
import com.rosetta.messenger.session.SessionState
|
import com.rosetta.messenger.session.SessionState
|
||||||
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
|
||||||
@@ -97,15 +102,32 @@ import com.rosetta.messenger.ui.settings.UpdatesScreen
|
|||||||
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 java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.io.File
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.UUID
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class MainActivity : FragmentActivity() {
|
class MainActivity : FragmentActivity() {
|
||||||
private lateinit var preferencesManager: PreferencesManager
|
@Inject lateinit var preferencesManager: PreferencesManager
|
||||||
private lateinit var accountManager: AccountManager
|
@Inject lateinit var accountManager: AccountManager
|
||||||
|
@Inject lateinit var messageRepository: MessageRepository
|
||||||
|
@Inject lateinit var groupRepository: GroupRepository
|
||||||
|
@Inject lateinit var protocolGateway: ProtocolGateway
|
||||||
|
@Inject lateinit var sessionCoordinator: SessionCoordinator
|
||||||
|
@Inject lateinit var identityGateway: IdentityGateway
|
||||||
|
|
||||||
|
private data class SharedPayload(
|
||||||
|
val text: String = "",
|
||||||
|
val streamUris: List<Uri> = emptyList()
|
||||||
|
)
|
||||||
|
private var pendingSharedPayload by mutableStateOf<SharedPayload?>(null)
|
||||||
|
|
||||||
// Флаг: Activity открыта для ответа на звонок с lock screen — пропускаем auth
|
// Флаг: Activity открыта для ответа на звонок с lock screen — пропускаем auth
|
||||||
// mutableStateOf чтобы Compose реагировал на изменение (избежать race condition)
|
// mutableStateOf чтобы Compose реагировал на изменение (избежать race condition)
|
||||||
@@ -151,13 +173,12 @@ class MainActivity : FragmentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
handleCallLockScreen(intent)
|
handleCallLockScreen(intent)
|
||||||
|
pendingSharedPayload = extractSharedPayload(intent)
|
||||||
|
|
||||||
preferencesManager = PreferencesManager(this)
|
|
||||||
accountManager = AccountManager(this)
|
|
||||||
RecentSearchesManager.init(this)
|
RecentSearchesManager.init(this)
|
||||||
|
|
||||||
// 🔥 Инициализируем ProtocolManager для обработки онлайн статусов
|
// 🔥 Инициализируем ProtocolManager для обработки онлайн статусов
|
||||||
ProtocolManager.initialize(this)
|
protocolGateway.initialize(this)
|
||||||
CallManager.initialize(this)
|
CallManager.initialize(this)
|
||||||
com.rosetta.messenger.ui.chats.components.AttachmentDownloadDebugLogger.init(this)
|
com.rosetta.messenger.ui.chats.components.AttachmentDownloadDebugLogger.init(this)
|
||||||
|
|
||||||
@@ -237,13 +258,13 @@ class MainActivity : FragmentActivity() {
|
|||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
val isLoggedIn by accountManager.isLoggedIn.collectAsState(initial = null)
|
val isLoggedIn by accountManager.isLoggedIn.collectAsState(initial = null)
|
||||||
val protocolState by ProtocolManager.state.collectAsState()
|
val protocolState by protocolGateway.state.collectAsState()
|
||||||
var showSplash by remember { mutableStateOf(true) }
|
var showSplash by remember { mutableStateOf(true) }
|
||||||
var showOnboarding by remember { mutableStateOf(true) }
|
var showOnboarding by remember { mutableStateOf(true) }
|
||||||
var hasExistingAccount by remember { mutableStateOf<Boolean?>(null) }
|
var hasExistingAccount by remember { mutableStateOf<Boolean?>(null) }
|
||||||
var currentAccount by remember { mutableStateOf(getCachedSessionAccount()) }
|
var currentAccount by remember { mutableStateOf(getCachedSessionAccount()) }
|
||||||
val identityState by IdentityStore.state.collectAsState()
|
val identityState by identityGateway.state.collectAsState()
|
||||||
val sessionState by AppSessionCoordinator.sessionState.collectAsState()
|
val sessionState by sessionCoordinator.sessionState.collectAsState()
|
||||||
var accountInfoList by remember { mutableStateOf<List<AccountInfo>>(emptyList()) }
|
var accountInfoList by remember { mutableStateOf<List<AccountInfo>>(emptyList()) }
|
||||||
var startCreateAccountFlow by remember { mutableStateOf(false) }
|
var startCreateAccountFlow by remember { mutableStateOf(false) }
|
||||||
var preservedMainNavStack by remember { mutableStateOf<List<Screen>>(emptyList()) }
|
var preservedMainNavStack by remember { mutableStateOf<List<Screen>>(emptyList()) }
|
||||||
@@ -251,7 +272,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
|
|
||||||
// Check for existing accounts and build AccountInfo list
|
// Check for existing accounts and build AccountInfo list
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
AppSessionCoordinator.syncFromCachedAccount(currentAccount)
|
sessionCoordinator.syncFromCachedAccount(currentAccount)
|
||||||
val accounts = accountManager.getAllAccounts()
|
val accounts = accountManager.getAllAccounts()
|
||||||
hasExistingAccount = accounts.isNotEmpty()
|
hasExistingAccount = accounts.isNotEmpty()
|
||||||
val infos = accounts.map { it.toAccountInfo() }
|
val infos = accounts.map { it.toAccountInfo() }
|
||||||
@@ -302,10 +323,50 @@ class MainActivity : FragmentActivity() {
|
|||||||
LaunchedEffect(currentAccount, isLoggedIn) {
|
LaunchedEffect(currentAccount, isLoggedIn) {
|
||||||
val account = currentAccount
|
val account = currentAccount
|
||||||
when {
|
when {
|
||||||
account != null -> AppSessionCoordinator.markReady(account, reason = "main_activity_state")
|
account != null -> sessionCoordinator.markReady(account, reason = "main_activity_state")
|
||||||
isLoggedIn == true ->
|
isLoggedIn == true ->
|
||||||
AppSessionCoordinator.markAuthInProgress(reason = "main_activity_logged_in_no_account")
|
sessionCoordinator.markAuthInProgress(reason = "main_activity_logged_in_no_account")
|
||||||
isLoggedIn == false -> AppSessionCoordinator.markLoggedOut(reason = "main_activity_logged_out")
|
isLoggedIn == false -> sessionCoordinator.markLoggedOut(reason = "main_activity_logged_out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(pendingSharedPayload, currentAccount?.publicKey, sessionState) {
|
||||||
|
val payload = pendingSharedPayload ?: return@LaunchedEffect
|
||||||
|
val account =
|
||||||
|
currentAccount ?: (sessionState as? SessionState.Ready)?.account ?: return@LaunchedEffect
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
val attachments =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
payload.streamUris.mapNotNull { sourceUri ->
|
||||||
|
buildSharedAttachment(sourceUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val messageText = payload.text.trim()
|
||||||
|
if (messageText.isBlank() && attachments.isEmpty()) {
|
||||||
|
return@runCatching
|
||||||
|
}
|
||||||
|
|
||||||
|
messageRepository.initialize(account.publicKey, account.privateKey)
|
||||||
|
messageRepository.sendMessage(
|
||||||
|
toPublicKey = account.publicKey,
|
||||||
|
text = messageText,
|
||||||
|
attachments = attachments
|
||||||
|
)
|
||||||
|
}.onSuccess {
|
||||||
|
pendingSharedPayload = null
|
||||||
|
val hadFiles = payload.streamUris.isNotEmpty()
|
||||||
|
Toast.makeText(
|
||||||
|
this@MainActivity,
|
||||||
|
if (hadFiles) {
|
||||||
|
"Shared content saved to Saved Messages"
|
||||||
|
} else {
|
||||||
|
"Shared text saved to Saved Messages"
|
||||||
|
},
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}.onFailure { error ->
|
||||||
|
android.util.Log.e(TAG, "Failed to import shared payload", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,6 +511,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
hasExistingAccount = screen == "auth_unlock",
|
hasExistingAccount = screen == "auth_unlock",
|
||||||
accounts = accountInfoList,
|
accounts = accountInfoList,
|
||||||
accountManager = accountManager,
|
accountManager = accountManager,
|
||||||
|
sessionCoordinator = sessionCoordinator,
|
||||||
startInCreateMode = startCreateAccountFlow,
|
startInCreateMode = startCreateAccountFlow,
|
||||||
onAuthComplete = { account ->
|
onAuthComplete = { account ->
|
||||||
startCreateAccountFlow = false
|
startCreateAccountFlow = false
|
||||||
@@ -481,11 +543,11 @@ class MainActivity : FragmentActivity() {
|
|||||||
currentAccount = normalizedAccount
|
currentAccount = normalizedAccount
|
||||||
cacheSessionAccount(normalizedAccount)
|
cacheSessionAccount(normalizedAccount)
|
||||||
normalizedAccount?.let {
|
normalizedAccount?.let {
|
||||||
AppSessionCoordinator.markReady(
|
sessionCoordinator.markReady(
|
||||||
account = it,
|
account = it,
|
||||||
reason = "auth_complete"
|
reason = "auth_complete"
|
||||||
)
|
)
|
||||||
} ?: AppSessionCoordinator.markAuthInProgress(
|
} ?: sessionCoordinator.markAuthInProgress(
|
||||||
reason = "auth_complete_no_account"
|
reason = "auth_complete_no_account"
|
||||||
)
|
)
|
||||||
hasExistingAccount = true
|
hasExistingAccount = true
|
||||||
@@ -526,11 +588,10 @@ class MainActivity : FragmentActivity() {
|
|||||||
// lag
|
// lag
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
clearCachedSessionAccount()
|
clearCachedSessionAccount()
|
||||||
AppSessionCoordinator.markLoggedOut(
|
sessionCoordinator.markLoggedOut(
|
||||||
reason = "auth_flow_logout"
|
reason = "auth_flow_logout"
|
||||||
)
|
)
|
||||||
com.rosetta.messenger.network.ProtocolManager
|
protocolGateway.disconnect()
|
||||||
.disconnect()
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
accountManager.logout()
|
accountManager.logout()
|
||||||
}
|
}
|
||||||
@@ -556,6 +617,11 @@ class MainActivity : FragmentActivity() {
|
|||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
},
|
},
|
||||||
|
accountManager = accountManager,
|
||||||
|
preferencesManager = preferencesManager,
|
||||||
|
groupRepository = groupRepository,
|
||||||
|
protocolGateway = protocolGateway,
|
||||||
|
identityGateway = identityGateway,
|
||||||
onNavStackChanged = { stack ->
|
onNavStackChanged = { stack ->
|
||||||
if (activeAccountKey.isNotBlank()) {
|
if (activeAccountKey.isNotBlank()) {
|
||||||
preservedMainNavAccountKey = activeAccountKey
|
preservedMainNavAccountKey = activeAccountKey
|
||||||
@@ -581,11 +647,10 @@ class MainActivity : FragmentActivity() {
|
|||||||
// lag
|
// lag
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
clearCachedSessionAccount()
|
clearCachedSessionAccount()
|
||||||
AppSessionCoordinator.markLoggedOut(
|
sessionCoordinator.markLoggedOut(
|
||||||
reason = "main_logout"
|
reason = "main_logout"
|
||||||
)
|
)
|
||||||
com.rosetta.messenger.network.ProtocolManager
|
protocolGateway.disconnect()
|
||||||
.disconnect()
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
accountManager.logout()
|
accountManager.logout()
|
||||||
}
|
}
|
||||||
@@ -609,7 +674,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
// 5. Delete account from Room DB
|
// 5. Delete account from Room DB
|
||||||
database.accountDao().deleteAccount(publicKey)
|
database.accountDao().deleteAccount(publicKey)
|
||||||
// 6. Disconnect protocol
|
// 6. Disconnect protocol
|
||||||
com.rosetta.messenger.network.ProtocolManager.disconnect()
|
protocolGateway.disconnect()
|
||||||
// 7. Delete account from AccountManager DataStore (removes from accounts list + clears login)
|
// 7. Delete account from AccountManager DataStore (removes from accounts list + clears login)
|
||||||
accountManager.deleteAccount(publicKey)
|
accountManager.deleteAccount(publicKey)
|
||||||
// 8. Refresh accounts list
|
// 8. Refresh accounts list
|
||||||
@@ -619,7 +684,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
// 8. Navigate away last
|
// 8. Navigate away last
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
clearCachedSessionAccount()
|
clearCachedSessionAccount()
|
||||||
AppSessionCoordinator.markLoggedOut(
|
sessionCoordinator.markLoggedOut(
|
||||||
reason = "delete_current_account"
|
reason = "delete_current_account"
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -649,7 +714,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
database.accountDao().deleteAccount(targetPublicKey)
|
database.accountDao().deleteAccount(targetPublicKey)
|
||||||
// 6. Disconnect protocol only if deleting currently open account
|
// 6. Disconnect protocol only if deleting currently open account
|
||||||
if (currentAccount?.publicKey == targetPublicKey) {
|
if (currentAccount?.publicKey == targetPublicKey) {
|
||||||
com.rosetta.messenger.network.ProtocolManager.disconnect()
|
protocolGateway.disconnect()
|
||||||
}
|
}
|
||||||
// 7. Delete account from AccountManager DataStore
|
// 7. Delete account from AccountManager DataStore
|
||||||
accountManager.deleteAccount(targetPublicKey)
|
accountManager.deleteAccount(targetPublicKey)
|
||||||
@@ -663,7 +728,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
preservedMainNavAccountKey = ""
|
preservedMainNavAccountKey = ""
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
clearCachedSessionAccount()
|
clearCachedSessionAccount()
|
||||||
AppSessionCoordinator.markLoggedOut(
|
sessionCoordinator.markLoggedOut(
|
||||||
reason = "delete_sidebar_current_account"
|
reason = "delete_sidebar_current_account"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -683,10 +748,10 @@ class MainActivity : FragmentActivity() {
|
|||||||
// Switch to another account: logout current, then show unlock.
|
// Switch to another account: logout current, then show unlock.
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
clearCachedSessionAccount()
|
clearCachedSessionAccount()
|
||||||
AppSessionCoordinator.markLoggedOut(
|
sessionCoordinator.markLoggedOut(
|
||||||
reason = "switch_account"
|
reason = "switch_account"
|
||||||
)
|
)
|
||||||
com.rosetta.messenger.network.ProtocolManager.disconnect()
|
protocolGateway.disconnect()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
accountManager.logout()
|
accountManager.logout()
|
||||||
}
|
}
|
||||||
@@ -697,10 +762,10 @@ class MainActivity : FragmentActivity() {
|
|||||||
preservedMainNavAccountKey = ""
|
preservedMainNavAccountKey = ""
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
clearCachedSessionAccount()
|
clearCachedSessionAccount()
|
||||||
AppSessionCoordinator.markLoggedOut(
|
sessionCoordinator.markLoggedOut(
|
||||||
reason = "add_account"
|
reason = "add_account"
|
||||||
)
|
)
|
||||||
com.rosetta.messenger.network.ProtocolManager.disconnect()
|
protocolGateway.disconnect()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
accountManager.logout()
|
accountManager.logout()
|
||||||
}
|
}
|
||||||
@@ -715,10 +780,10 @@ class MainActivity : FragmentActivity() {
|
|||||||
preservedMainNavAccountKey = ""
|
preservedMainNavAccountKey = ""
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
clearCachedSessionAccount()
|
clearCachedSessionAccount()
|
||||||
AppSessionCoordinator.markLoggedOut(
|
sessionCoordinator.markLoggedOut(
|
||||||
reason = "device_confirm_exit"
|
reason = "device_confirm_exit"
|
||||||
)
|
)
|
||||||
ProtocolManager.disconnect()
|
protocolGateway.disconnect()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
accountManager.logout()
|
accountManager.logout()
|
||||||
}
|
}
|
||||||
@@ -734,7 +799,134 @@ class MainActivity : FragmentActivity() {
|
|||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
handleCallLockScreen(intent)
|
handleCallLockScreen(intent)
|
||||||
|
pendingSharedPayload = extractSharedPayload(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractSharedPayload(intent: Intent?): SharedPayload? {
|
||||||
|
if (intent == null) return null
|
||||||
|
val action = intent.action ?: return null
|
||||||
|
if (action != Intent.ACTION_SEND && action != Intent.ACTION_SEND_MULTIPLE) return null
|
||||||
|
|
||||||
|
val sharedText =
|
||||||
|
(
|
||||||
|
intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
|
?: intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
|
||||||
|
?: intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
||||||
|
)
|
||||||
|
?.trim()
|
||||||
|
.orEmpty()
|
||||||
|
|
||||||
|
val sharedUris =
|
||||||
|
when (action) {
|
||||||
|
Intent.ACTION_SEND -> {
|
||||||
|
listOfNotNull(getParcelableUriExtra(intent, Intent.EXTRA_STREAM))
|
||||||
|
}
|
||||||
|
Intent.ACTION_SEND_MULTIPLE -> {
|
||||||
|
getParcelableUriListExtra(intent, Intent.EXTRA_STREAM)
|
||||||
|
}
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sharedText.isBlank() && sharedUris.isEmpty()) return null
|
||||||
|
return SharedPayload(text = sharedText, streamUris = sharedUris)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getParcelableUriExtra(intent: Intent, key: String): Uri? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
intent.getParcelableExtra(key, Uri::class.java)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
intent.getParcelableExtra(key) as? Uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getParcelableUriListExtra(intent: Intent, key: String): List<Uri> {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
intent.getParcelableArrayListExtra(key, Uri::class.java)?.filterNotNull().orEmpty()
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
intent.getParcelableArrayListExtra<Uri>(key)?.filterNotNull().orEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSharedAttachment(sourceUri: Uri): MessageAttachment? {
|
||||||
|
val cachedUri = copySharedUriToCache(sourceUri) ?: return null
|
||||||
|
val mimeType = contentResolver.getType(sourceUri).orEmpty().lowercase(Locale.ROOT)
|
||||||
|
val fileName = queryDisplayName(sourceUri) ?: cachedUri.lastPathSegment ?: "shared_file"
|
||||||
|
val fileSize = querySize(sourceUri) ?: runCatching { File(cachedUri.path ?: "").length() }.getOrNull() ?: 0L
|
||||||
|
val type = if (mimeType.startsWith("image/")) AttachmentType.IMAGE else AttachmentType.FILE
|
||||||
|
val preview =
|
||||||
|
if (type == AttachmentType.FILE) {
|
||||||
|
"${fileSize.coerceAtLeast(0L)}::$fileName"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
return MessageAttachment(
|
||||||
|
id = UUID.randomUUID().toString().replace("-", "").take(32),
|
||||||
|
blob = "",
|
||||||
|
type = type,
|
||||||
|
preview = preview,
|
||||||
|
localUri = cachedUri.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copySharedUriToCache(sourceUri: Uri): Uri? {
|
||||||
|
return runCatching {
|
||||||
|
val sourceName = queryDisplayName(sourceUri).orEmpty()
|
||||||
|
val extension =
|
||||||
|
sourceName.substringAfterLast('.', "").trim().takeIf { it.isNotBlank() }
|
||||||
|
val targetDir = File(cacheDir, "shared_import").apply { mkdirs() }
|
||||||
|
val targetFileName =
|
||||||
|
buildString {
|
||||||
|
append("shared_")
|
||||||
|
append(System.currentTimeMillis())
|
||||||
|
append('_')
|
||||||
|
append((1000..9999).random())
|
||||||
|
if (extension != null) {
|
||||||
|
append('.')
|
||||||
|
append(extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val targetFile = File(targetDir, targetFileName)
|
||||||
|
|
||||||
|
contentResolver.openInputStream(sourceUri)?.use { input ->
|
||||||
|
targetFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
Uri.fromFile(targetFile)
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queryDisplayName(uri: Uri): String? {
|
||||||
|
return runCatching {
|
||||||
|
contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)
|
||||||
|
?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
cursor.getString(0)?.trim()?.ifBlank { null }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun querySize(uri: Uri): Long? {
|
||||||
|
return runCatching {
|
||||||
|
contentResolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null)
|
||||||
|
?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
val size = cursor.getLong(0)
|
||||||
|
if (size >= 0L) size else null
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var callIntentResetJob: kotlinx.coroutines.Job? = null
|
private var callIntentResetJob: kotlinx.coroutines.Job? = null
|
||||||
@@ -788,7 +980,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
// 🔔 Сбрасываем все уведомления из шторки при открытии приложения
|
// 🔔 Сбрасываем все уведомления из шторки при открытии приложения
|
||||||
(getSystemService(NOTIFICATION_SERVICE) as android.app.NotificationManager).cancelAll()
|
(getSystemService(NOTIFICATION_SERVICE) as android.app.NotificationManager).cancelAll()
|
||||||
// ⚡ На возврате в приложение пробуем мгновенный reconnect без ожидания backoff.
|
// ⚡ На возврате в приложение пробуем мгновенный reconnect без ожидания backoff.
|
||||||
ProtocolManager.reconnectNowIfNeeded("activity_onResume")
|
protocolGateway.reconnectNowIfNeeded("activity_onResume")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -822,9 +1014,9 @@ class MainActivity : FragmentActivity() {
|
|||||||
// Сохраняем токен локально
|
// Сохраняем токен локально
|
||||||
saveFcmToken(token)
|
saveFcmToken(token)
|
||||||
addFcmLog("💾 Токен сохранен локально")
|
addFcmLog("💾 Токен сохранен локально")
|
||||||
if (ProtocolManager.isAuthenticated()) {
|
if (protocolGateway.isAuthenticated()) {
|
||||||
runCatching {
|
runCatching {
|
||||||
ProtocolManager.subscribePushTokenIfAvailable(
|
protocolGateway.subscribePushTokenIfAvailable(
|
||||||
forceToken = token
|
forceToken = token
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -926,6 +1118,11 @@ fun MainScreen(
|
|||||||
account: DecryptedAccount? = null,
|
account: DecryptedAccount? = null,
|
||||||
initialNavStack: List<Screen> = emptyList(),
|
initialNavStack: List<Screen> = emptyList(),
|
||||||
onNavStackChanged: (List<Screen>) -> Unit = {},
|
onNavStackChanged: (List<Screen>) -> Unit = {},
|
||||||
|
accountManager: AccountManager,
|
||||||
|
preferencesManager: PreferencesManager,
|
||||||
|
groupRepository: GroupRepository,
|
||||||
|
protocolGateway: ProtocolGateway,
|
||||||
|
identityGateway: IdentityGateway,
|
||||||
isDarkTheme: Boolean = true,
|
isDarkTheme: Boolean = true,
|
||||||
themeMode: String = "dark",
|
themeMode: String = "dark",
|
||||||
onToggleTheme: () -> Unit = {},
|
onToggleTheme: () -> Unit = {},
|
||||||
@@ -955,7 +1152,7 @@ fun MainScreen(
|
|||||||
// Following desktop version pattern: username is stored locally and loaded on app start
|
// Following desktop version pattern: username is stored locally and loaded on app start
|
||||||
var accountUsername by remember { mutableStateOf("") }
|
var accountUsername by remember { mutableStateOf("") }
|
||||||
var accountVerified by remember(accountPublicKey) { mutableIntStateOf(0) }
|
var accountVerified by remember(accountPublicKey) { mutableIntStateOf(0) }
|
||||||
val identitySnapshot by IdentityStore.state.collectAsState()
|
val identitySnapshot by identityGateway.state.collectAsState()
|
||||||
var reloadTrigger by remember { mutableIntStateOf(0) }
|
var reloadTrigger by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
// Load username AND name from AccountManager (persisted in DataStore)
|
// Load username AND name from AccountManager (persisted in DataStore)
|
||||||
@@ -1031,11 +1228,11 @@ fun MainScreen(
|
|||||||
return@resolve null
|
return@resolve null
|
||||||
}
|
}
|
||||||
|
|
||||||
ProtocolManager.getCachedUserByUsername(usernameQuery)?.let { cached ->
|
protocolGateway.getCachedUserByUsername(usernameQuery)?.let { cached ->
|
||||||
if (cached.publicKey.isNotBlank()) return@resolve cached
|
if (cached.publicKey.isNotBlank()) return@resolve cached
|
||||||
}
|
}
|
||||||
|
|
||||||
val results = ProtocolManager.searchUsers(usernameQuery)
|
val results = protocolGateway.searchUsers(usernameQuery)
|
||||||
results.firstOrNull {
|
results.firstOrNull {
|
||||||
it.publicKey.isNotBlank() &&
|
it.publicKey.isNotBlank() &&
|
||||||
it.username.trim().trimStart('@')
|
it.username.trim().trimStart('@')
|
||||||
@@ -1143,7 +1340,7 @@ fun MainScreen(
|
|||||||
val normalizedPublicKey = accountPublicKey.trim()
|
val normalizedPublicKey = accountPublicKey.trim()
|
||||||
val normalizedPrivateKey = accountPrivateKey.trim()
|
val normalizedPrivateKey = accountPrivateKey.trim()
|
||||||
if (normalizedPublicKey.isBlank() || normalizedPrivateKey.isBlank()) return@LaunchedEffect
|
if (normalizedPublicKey.isBlank() || normalizedPrivateKey.isBlank()) return@LaunchedEffect
|
||||||
ProtocolManager.initializeAccount(normalizedPublicKey, normalizedPrivateKey)
|
protocolGateway.initializeAccount(normalizedPublicKey, normalizedPrivateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(callUiState.isVisible) {
|
LaunchedEffect(callUiState.isVisible) {
|
||||||
@@ -1197,13 +1394,12 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun refreshAccountIdentityState(accountKey: String) {
|
suspend fun refreshAccountIdentityState(accountKey: String) {
|
||||||
val accountManager = AccountManager(context)
|
|
||||||
val encryptedAccount = accountManager.getAccount(accountKey)
|
val encryptedAccount = accountManager.getAccount(accountKey)
|
||||||
val identityOwn =
|
val identityOwn =
|
||||||
identitySnapshot.profile?.takeIf {
|
identitySnapshot.profile?.takeIf {
|
||||||
it.publicKey.equals(accountKey, ignoreCase = true)
|
it.publicKey.equals(accountKey, ignoreCase = true)
|
||||||
}
|
}
|
||||||
val cachedOwn = ProtocolManager.getCachedUserInfo(accountKey)
|
val cachedOwn = protocolGateway.getCachedUserInfo(accountKey)
|
||||||
|
|
||||||
val persistedName = encryptedAccount?.name?.trim().orEmpty()
|
val persistedName = encryptedAccount?.name?.trim().orEmpty()
|
||||||
val persistedUsername = encryptedAccount?.username?.trim().orEmpty()
|
val persistedUsername = encryptedAccount?.username?.trim().orEmpty()
|
||||||
@@ -1237,7 +1433,7 @@ fun MainScreen(
|
|||||||
accountUsername = finalUsername
|
accountUsername = finalUsername
|
||||||
accountVerified = identityOwn?.verified ?: cachedOwn?.verified ?: 0
|
accountVerified = identityOwn?.verified ?: cachedOwn?.verified ?: 0
|
||||||
accountName = resolveAccountDisplayName(accountKey, preferredName, finalUsername)
|
accountName = resolveAccountDisplayName(accountKey, preferredName, finalUsername)
|
||||||
IdentityStore.updateOwnProfile(
|
identityGateway.updateOwnProfile(
|
||||||
publicKey = accountKey,
|
publicKey = accountKey,
|
||||||
displayName = accountName,
|
displayName = accountName,
|
||||||
username = accountUsername,
|
username = accountUsername,
|
||||||
@@ -1271,10 +1467,10 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Состояние протокола для передачи в SearchScreen
|
// Состояние протокола для передачи в SearchScreen
|
||||||
val protocolState by ProtocolManager.state.collectAsState()
|
val protocolState by protocolGateway.state.collectAsState()
|
||||||
|
|
||||||
// Реактивно обновляем username/name когда сервер отвечает на fetchOwnProfile()
|
// Реактивно обновляем username/name когда сервер отвечает на fetchOwnProfile()
|
||||||
val ownProfileUpdated by ProtocolManager.ownProfileUpdated.collectAsState()
|
val ownProfileUpdated by protocolGateway.ownProfileUpdated.collectAsState()
|
||||||
LaunchedEffect(ownProfileUpdated, accountPublicKey) {
|
LaunchedEffect(ownProfileUpdated, accountPublicKey) {
|
||||||
if (ownProfileUpdated > 0L && accountPublicKey.isNotBlank()) {
|
if (ownProfileUpdated > 0L && accountPublicKey.isNotBlank()) {
|
||||||
refreshAccountIdentityState(accountPublicKey)
|
refreshAccountIdentityState(accountPublicKey)
|
||||||
@@ -1440,7 +1636,7 @@ fun MainScreen(
|
|||||||
androidx.lifecycle.viewmodel.compose.viewModel()
|
androidx.lifecycle.viewmodel.compose.viewModel()
|
||||||
|
|
||||||
// Appearance: background blur color preference
|
// Appearance: background blur color preference
|
||||||
val prefsManager = remember { com.rosetta.messenger.data.PreferencesManager(context) }
|
val prefsManager = preferencesManager
|
||||||
val backgroundBlurColorId by
|
val backgroundBlurColorId by
|
||||||
prefsManager
|
prefsManager
|
||||||
.backgroundBlurColorIdForAccount(accountPublicKey)
|
.backgroundBlurColorIdForAccount(accountPublicKey)
|
||||||
@@ -1669,7 +1865,6 @@ fun MainScreen(
|
|||||||
// Verify password by trying to decrypt the private key
|
// Verify password by trying to decrypt the private key
|
||||||
try {
|
try {
|
||||||
val publicKey = account?.publicKey ?: return@BackupScreen null
|
val publicKey = account?.publicKey ?: return@BackupScreen null
|
||||||
val accountManager = AccountManager(context)
|
|
||||||
val encryptedAccount = accountManager.getAccount(publicKey)
|
val encryptedAccount = accountManager.getAccount(publicKey)
|
||||||
|
|
||||||
if (encryptedAccount != null) {
|
if (encryptedAccount != null) {
|
||||||
@@ -2075,7 +2270,6 @@ fun MainScreen(
|
|||||||
val biometricPrefs = remember {
|
val biometricPrefs = remember {
|
||||||
com.rosetta.messenger.biometric.BiometricPreferences(context)
|
com.rosetta.messenger.biometric.BiometricPreferences(context)
|
||||||
}
|
}
|
||||||
val biometricAccountManager = remember { AccountManager(context) }
|
|
||||||
val activity = context as? FragmentActivity
|
val activity = context as? FragmentActivity
|
||||||
val isFingerprintSupported = remember {
|
val isFingerprintSupported = remember {
|
||||||
biometricManager.isFingerprintHardwareAvailable()
|
biometricManager.isFingerprintHardwareAvailable()
|
||||||
@@ -2099,7 +2293,7 @@ fun MainScreen(
|
|||||||
|
|
||||||
// Verify password against the real account before saving
|
// Verify password against the real account before saving
|
||||||
mainScreenScope.launch {
|
mainScreenScope.launch {
|
||||||
val account = biometricAccountManager.getAccount(accountPublicKey)
|
val account = accountManager.getAccount(accountPublicKey)
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
onError("Account not found")
|
onError("Account not found")
|
||||||
return@launch
|
return@launch
|
||||||
@@ -2149,7 +2343,7 @@ fun MainScreen(
|
|||||||
when (result.type) {
|
when (result.type) {
|
||||||
com.rosetta.messenger.ui.qr.QrResultType.PROFILE -> {
|
com.rosetta.messenger.ui.qr.QrResultType.PROFILE -> {
|
||||||
mainScreenScope.launch {
|
mainScreenScope.launch {
|
||||||
val users = com.rosetta.messenger.network.ProtocolManager.searchUsers(result.payload, 5000)
|
val users = protocolGateway.searchUsers(result.payload, 5000)
|
||||||
val user = users.firstOrNull()
|
val user = users.firstOrNull()
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
pushScreen(Screen.OtherProfile(user))
|
pushScreen(Screen.OtherProfile(user))
|
||||||
@@ -2164,8 +2358,8 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
com.rosetta.messenger.ui.qr.QrResultType.GROUP -> {
|
com.rosetta.messenger.ui.qr.QrResultType.GROUP -> {
|
||||||
mainScreenScope.launch {
|
mainScreenScope.launch {
|
||||||
val groupRepo = com.rosetta.messenger.data.GroupRepository.getInstance(context)
|
val joinResult =
|
||||||
val joinResult = groupRepo.joinGroup(accountPublicKey, accountPrivateKey, result.payload)
|
groupRepository.joinGroup(accountPublicKey, accountPrivateKey, result.payload)
|
||||||
if (joinResult.success && !joinResult.dialogPublicKey.isNullOrBlank()) {
|
if (joinResult.success && !joinResult.dialogPublicKey.isNullOrBlank()) {
|
||||||
val groupUser = com.rosetta.messenger.network.SearchUser(
|
val groupUser = com.rosetta.messenger.network.SearchUser(
|
||||||
publicKey = joinResult.dialogPublicKey,
|
publicKey = joinResult.dialogPublicKey,
|
||||||
|
|||||||
@@ -2,15 +2,27 @@ package com.rosetta.messenger
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.airbnb.lottie.L
|
import com.airbnb.lottie.L
|
||||||
|
import com.rosetta.messenger.data.AccountManager
|
||||||
import com.rosetta.messenger.data.DraftManager
|
import com.rosetta.messenger.data.DraftManager
|
||||||
|
import com.rosetta.messenger.data.GroupRepository
|
||||||
|
import com.rosetta.messenger.data.MessageRepository
|
||||||
|
import com.rosetta.messenger.network.CallManager
|
||||||
|
import com.rosetta.messenger.network.ProtocolManager
|
||||||
import com.rosetta.messenger.network.TransportManager
|
import com.rosetta.messenger.network.TransportManager
|
||||||
import com.rosetta.messenger.update.UpdateManager
|
import com.rosetta.messenger.update.UpdateManager
|
||||||
import com.rosetta.messenger.utils.CrashReportManager
|
import com.rosetta.messenger.utils.CrashReportManager
|
||||||
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application класс для инициализации глобальных компонентов приложения
|
* Application класс для инициализации глобальных компонентов приложения
|
||||||
*/
|
*/
|
||||||
|
@HiltAndroidApp
|
||||||
class RosettaApplication : Application() {
|
class RosettaApplication : Application() {
|
||||||
|
|
||||||
|
@Inject lateinit var messageRepository: MessageRepository
|
||||||
|
@Inject lateinit var groupRepository: GroupRepository
|
||||||
|
@Inject lateinit var accountManager: AccountManager
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "RosettaApplication"
|
private const val TAG = "RosettaApplication"
|
||||||
@@ -33,6 +45,17 @@ class RosettaApplication : Application() {
|
|||||||
|
|
||||||
// Инициализируем менеджер обновлений (SDU)
|
// Инициализируем менеджер обновлений (SDU)
|
||||||
UpdateManager.init(this)
|
UpdateManager.init(this)
|
||||||
|
|
||||||
|
// DI bootstrap for protocol internals (removes singleton-factory lookups in ProtocolManager).
|
||||||
|
ProtocolManager.bindDependencies(
|
||||||
|
messageRepository = messageRepository,
|
||||||
|
groupRepository = groupRepository,
|
||||||
|
accountManager = accountManager
|
||||||
|
)
|
||||||
|
CallManager.bindDependencies(
|
||||||
|
messageRepository = messageRepository,
|
||||||
|
accountManager = accountManager
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,16 +15,22 @@ import com.rosetta.messenger.network.PacketGroupInviteInfo
|
|||||||
import com.rosetta.messenger.network.PacketGroupJoin
|
import com.rosetta.messenger.network.PacketGroupJoin
|
||||||
import com.rosetta.messenger.network.PacketGroupLeave
|
import com.rosetta.messenger.network.PacketGroupLeave
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
import com.rosetta.messenger.network.ProtocolManager
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
class GroupRepository private constructor(context: Context) {
|
@Singleton
|
||||||
|
class GroupRepository @Inject constructor(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
private val messageRepository: MessageRepository
|
||||||
|
) {
|
||||||
|
|
||||||
private val appContext = context.applicationContext
|
|
||||||
private val db = RosettaDatabase.getDatabase(context.applicationContext)
|
private val db = RosettaDatabase.getDatabase(context.applicationContext)
|
||||||
private val groupDao = db.groupDao()
|
private val groupDao = db.groupDao()
|
||||||
private val messageDao = db.messageDao()
|
private val messageDao = db.messageDao()
|
||||||
@@ -38,15 +44,6 @@ class GroupRepository private constructor(context: Context) {
|
|||||||
private const val GROUP_INVITE_PASSWORD = "rosetta_group"
|
private const val GROUP_INVITE_PASSWORD = "rosetta_group"
|
||||||
private const val GROUP_WAIT_TIMEOUT_MS = 15_000L
|
private const val GROUP_WAIT_TIMEOUT_MS = 15_000L
|
||||||
private const val GROUP_CREATED_MARKER = "\$a=Group created"
|
private const val GROUP_CREATED_MARKER = "\$a=Group created"
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var INSTANCE: GroupRepository? = null
|
|
||||||
|
|
||||||
fun getInstance(context: Context): GroupRepository {
|
|
||||||
return INSTANCE ?: synchronized(this) {
|
|
||||||
INSTANCE ?: GroupRepository(context).also { INSTANCE = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ParsedGroupInvite(
|
data class ParsedGroupInvite(
|
||||||
@@ -479,9 +476,8 @@ class GroupRepository private constructor(context: Context) {
|
|||||||
dialogPublicKey: String
|
dialogPublicKey: String
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val messages = MessageRepository.getInstance(appContext)
|
messageRepository.initialize(accountPublicKey, accountPrivateKey)
|
||||||
messages.initialize(accountPublicKey, accountPrivateKey)
|
messageRepository.sendMessage(
|
||||||
messages.sendMessage(
|
|
||||||
toPublicKey = dialogPublicKey,
|
toPublicKey = dialogPublicKey,
|
||||||
text = GROUP_CREATED_MARKER
|
text = GROUP_CREATED_MARKER
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import com.rosetta.messenger.network.*
|
|||||||
import com.rosetta.messenger.utils.AttachmentFileManager
|
import com.rosetta.messenger.utils.AttachmentFileManager
|
||||||
import com.rosetta.messenger.utils.AvatarFileManager
|
import com.rosetta.messenger.utils.AvatarFileManager
|
||||||
import com.rosetta.messenger.utils.MessageLogger
|
import com.rosetta.messenger.utils.MessageLogger
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
@@ -43,7 +46,10 @@ data class Dialog(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/** Repository для работы с сообщениями Оптимизированная версия с кэшированием и Optimistic UI */
|
/** Repository для работы с сообщениями Оптимизированная версия с кэшированием и Optimistic UI */
|
||||||
class MessageRepository private constructor(private val context: Context) {
|
@Singleton
|
||||||
|
class MessageRepository @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context
|
||||||
|
) {
|
||||||
|
|
||||||
private val database = RosettaDatabase.getDatabase(context)
|
private val database = RosettaDatabase.getDatabase(context)
|
||||||
private val messageDao = database.messageDao()
|
private val messageDao = database.messageDao()
|
||||||
@@ -96,8 +102,6 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
private var currentPrivateKey: String? = null
|
private var currentPrivateKey: String? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile private var INSTANCE: MessageRepository? = null
|
|
||||||
|
|
||||||
/** Desktop parity: MESSAGE_MAX_TIME_TO_DELEVERED_S = 80 (seconds) */
|
/** Desktop parity: MESSAGE_MAX_TIME_TO_DELEVERED_S = 80 (seconds) */
|
||||||
private const val MESSAGE_MAX_TIME_TO_DELIVERED_MS = 80_000L
|
private const val MESSAGE_MAX_TIME_TO_DELIVERED_MS = 80_000L
|
||||||
|
|
||||||
@@ -135,16 +139,6 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
/** Очистка кэша (вызывается при logout) */
|
/** Очистка кэша (вызывается при logout) */
|
||||||
fun clearProcessedCache() = processedMessageIds.clear()
|
fun clearProcessedCache() = processedMessageIds.clear()
|
||||||
|
|
||||||
fun getInstance(context: Context): MessageRepository {
|
|
||||||
return INSTANCE
|
|
||||||
?: synchronized(this) {
|
|
||||||
INSTANCE
|
|
||||||
?: MessageRepository(context.applicationContext).also {
|
|
||||||
INSTANCE = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Генерация уникального messageId 🔥 ИСПРАВЛЕНО: Используем UUID вместо детерминированного
|
* Генерация уникального messageId 🔥 ИСПРАВЛЕНО: Используем UUID вместо детерминированного
|
||||||
* хэша, чтобы избежать коллизий при одинаковом timestamp (при спаме сообщениями)
|
* хэша, чтобы избежать коллизий при одинаковом timestamp (при спаме сообщениями)
|
||||||
@@ -1785,6 +1779,7 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
put("preview", attachment.preview)
|
put("preview", attachment.preview)
|
||||||
put("width", attachment.width)
|
put("width", attachment.width)
|
||||||
put("height", attachment.height)
|
put("height", attachment.height)
|
||||||
|
put("localUri", attachment.localUri)
|
||||||
put("transportTag", attachment.transportTag)
|
put("transportTag", attachment.transportTag)
|
||||||
put("transportServer", attachment.transportServer)
|
put("transportServer", attachment.transportServer)
|
||||||
}
|
}
|
||||||
@@ -2029,6 +2024,7 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
jsonObj.put("preview", attachment.preview)
|
jsonObj.put("preview", attachment.preview)
|
||||||
jsonObj.put("width", attachment.width)
|
jsonObj.put("width", attachment.width)
|
||||||
jsonObj.put("height", attachment.height)
|
jsonObj.put("height", attachment.height)
|
||||||
|
jsonObj.put("localUri", attachment.localUri)
|
||||||
jsonObj.put("transportTag", attachment.transportTag)
|
jsonObj.put("transportTag", attachment.transportTag)
|
||||||
jsonObj.put("transportServer", attachment.transportServer)
|
jsonObj.put("transportServer", attachment.transportServer)
|
||||||
} else {
|
} else {
|
||||||
@@ -2039,6 +2035,7 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
jsonObj.put("preview", attachment.preview)
|
jsonObj.put("preview", attachment.preview)
|
||||||
jsonObj.put("width", attachment.width)
|
jsonObj.put("width", attachment.width)
|
||||||
jsonObj.put("height", attachment.height)
|
jsonObj.put("height", attachment.height)
|
||||||
|
jsonObj.put("localUri", attachment.localUri)
|
||||||
jsonObj.put("transportTag", attachment.transportTag)
|
jsonObj.put("transportTag", attachment.transportTag)
|
||||||
jsonObj.put("transportServer", attachment.transportServer)
|
jsonObj.put("transportServer", attachment.transportServer)
|
||||||
}
|
}
|
||||||
@@ -2050,6 +2047,7 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
jsonObj.put("preview", attachment.preview)
|
jsonObj.put("preview", attachment.preview)
|
||||||
jsonObj.put("width", attachment.width)
|
jsonObj.put("width", attachment.width)
|
||||||
jsonObj.put("height", attachment.height)
|
jsonObj.put("height", attachment.height)
|
||||||
|
jsonObj.put("localUri", attachment.localUri)
|
||||||
jsonObj.put("transportTag", attachment.transportTag)
|
jsonObj.put("transportTag", attachment.transportTag)
|
||||||
jsonObj.put("transportServer", attachment.transportServer)
|
jsonObj.put("transportServer", attachment.transportServer)
|
||||||
}
|
}
|
||||||
@@ -2061,6 +2059,7 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
jsonObj.put("preview", attachment.preview)
|
jsonObj.put("preview", attachment.preview)
|
||||||
jsonObj.put("width", attachment.width)
|
jsonObj.put("width", attachment.width)
|
||||||
jsonObj.put("height", attachment.height)
|
jsonObj.put("height", attachment.height)
|
||||||
|
jsonObj.put("localUri", attachment.localUri)
|
||||||
jsonObj.put("transportTag", attachment.transportTag)
|
jsonObj.put("transportTag", attachment.transportTag)
|
||||||
jsonObj.put("transportServer", attachment.transportServer)
|
jsonObj.put("transportServer", attachment.transportServer)
|
||||||
}
|
}
|
||||||
|
|||||||
285
app/src/main/java/com/rosetta/messenger/di/AppContainer.kt
Normal file
285
app/src/main/java/com/rosetta/messenger/di/AppContainer.kt
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
package com.rosetta.messenger.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.rosetta.messenger.data.AccountManager
|
||||||
|
import com.rosetta.messenger.data.DecryptedAccount
|
||||||
|
import com.rosetta.messenger.data.GroupRepository
|
||||||
|
import com.rosetta.messenger.data.MessageRepository
|
||||||
|
import com.rosetta.messenger.data.PreferencesManager
|
||||||
|
import com.rosetta.messenger.network.DeviceEntry
|
||||||
|
import com.rosetta.messenger.network.Packet
|
||||||
|
import com.rosetta.messenger.network.PacketMessage
|
||||||
|
import com.rosetta.messenger.network.ProtocolManager
|
||||||
|
import com.rosetta.messenger.network.ProtocolState
|
||||||
|
import com.rosetta.messenger.network.SearchUser
|
||||||
|
import com.rosetta.messenger.session.SessionAction
|
||||||
|
import com.rosetta.messenger.session.IdentityStateSnapshot
|
||||||
|
import com.rosetta.messenger.session.IdentityStore
|
||||||
|
import com.rosetta.messenger.session.SessionState
|
||||||
|
import com.rosetta.messenger.session.SessionStore
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
interface ProtocolGateway {
|
||||||
|
val state: StateFlow<ProtocolState>
|
||||||
|
val syncInProgress: StateFlow<Boolean>
|
||||||
|
val pendingDeviceVerification: StateFlow<DeviceEntry?>
|
||||||
|
val typingUsers: StateFlow<Set<String>>
|
||||||
|
val typingUsersByDialogSnapshot: StateFlow<Map<String, Set<String>>>
|
||||||
|
val debugLogs: StateFlow<List<String>>
|
||||||
|
val ownProfileUpdated: StateFlow<Long>
|
||||||
|
|
||||||
|
fun initialize(context: Context)
|
||||||
|
fun initializeAccount(publicKey: String, privateKey: String)
|
||||||
|
fun connect()
|
||||||
|
fun authenticate(publicKey: String, privateHash: String)
|
||||||
|
fun reconnectNowIfNeeded(reason: String)
|
||||||
|
fun disconnect()
|
||||||
|
fun isAuthenticated(): Boolean
|
||||||
|
fun getPrivateHash(): String?
|
||||||
|
fun subscribePushTokenIfAvailable(forceToken: String? = null)
|
||||||
|
fun addLog(message: String)
|
||||||
|
fun enableUILogs(enabled: Boolean)
|
||||||
|
fun clearLogs()
|
||||||
|
fun resolveOutgoingRetry(messageId: String)
|
||||||
|
fun getCachedUserByUsername(username: String): SearchUser?
|
||||||
|
fun getCachedUserName(publicKey: String): String?
|
||||||
|
fun getCachedUserInfo(publicKey: String): SearchUser?
|
||||||
|
fun acceptDevice(deviceId: String)
|
||||||
|
fun declineDevice(deviceId: String)
|
||||||
|
fun send(packet: Packet)
|
||||||
|
fun sendPacket(packet: Packet)
|
||||||
|
fun sendMessageWithRetry(packet: PacketMessage)
|
||||||
|
fun waitPacket(packetId: Int, callback: (Packet) -> Unit)
|
||||||
|
fun unwaitPacket(packetId: Int, callback: (Packet) -> Unit)
|
||||||
|
fun packetFlow(packetId: Int): SharedFlow<Packet>
|
||||||
|
fun notifyOwnProfileUpdated()
|
||||||
|
fun restoreAuthFromStoredCredentials(
|
||||||
|
preferredPublicKey: String? = null,
|
||||||
|
reason: String = "background_restore"
|
||||||
|
): Boolean
|
||||||
|
suspend fun resolveUserName(publicKey: String, timeoutMs: Long = 3000): String?
|
||||||
|
suspend fun resolveUserInfo(publicKey: String, timeoutMs: Long = 3000): SearchUser?
|
||||||
|
suspend fun searchUsers(query: String, timeoutMs: Long = 3000): List<SearchUser>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionCoordinator {
|
||||||
|
val sessionState: StateFlow<SessionState>
|
||||||
|
|
||||||
|
fun dispatch(action: SessionAction)
|
||||||
|
fun markLoggedOut(reason: String = "") =
|
||||||
|
dispatch(SessionAction.LoggedOut(reason = reason))
|
||||||
|
fun markAuthInProgress(publicKey: String? = null, reason: String = "") =
|
||||||
|
dispatch(
|
||||||
|
SessionAction.AuthInProgress(
|
||||||
|
publicKey = publicKey,
|
||||||
|
reason = reason
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fun markReady(account: DecryptedAccount, reason: String = "") =
|
||||||
|
dispatch(SessionAction.Ready(account = account, reason = reason))
|
||||||
|
fun syncFromCachedAccount(account: DecryptedAccount?) =
|
||||||
|
dispatch(SessionAction.SyncFromCachedAccount(account = account))
|
||||||
|
suspend fun bootstrapAuthenticatedSession(account: DecryptedAccount, reason: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IdentityGateway {
|
||||||
|
val state: StateFlow<IdentityStateSnapshot>
|
||||||
|
|
||||||
|
fun updateOwnProfile(
|
||||||
|
publicKey: String,
|
||||||
|
displayName: String? = null,
|
||||||
|
username: String? = null,
|
||||||
|
verified: Int? = null,
|
||||||
|
resolved: Boolean = true,
|
||||||
|
reason: String = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ProtocolGatewayImpl @Inject constructor(
|
||||||
|
private val messageRepository: MessageRepository,
|
||||||
|
private val groupRepository: GroupRepository,
|
||||||
|
private val accountManager: AccountManager
|
||||||
|
) : ProtocolGateway {
|
||||||
|
init {
|
||||||
|
ProtocolManager.bindDependencies(
|
||||||
|
messageRepository = messageRepository,
|
||||||
|
groupRepository = groupRepository,
|
||||||
|
accountManager = accountManager
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val state: StateFlow<ProtocolState> = ProtocolManager.state
|
||||||
|
override val syncInProgress: StateFlow<Boolean> = ProtocolManager.syncInProgress
|
||||||
|
override val pendingDeviceVerification: StateFlow<DeviceEntry?> = ProtocolManager.pendingDeviceVerification
|
||||||
|
override val typingUsers: StateFlow<Set<String>> = ProtocolManager.typingUsers
|
||||||
|
override val typingUsersByDialogSnapshot: StateFlow<Map<String, Set<String>>> =
|
||||||
|
ProtocolManager.typingUsersByDialogSnapshot
|
||||||
|
override val debugLogs: StateFlow<List<String>> = ProtocolManager.debugLogs
|
||||||
|
override val ownProfileUpdated: StateFlow<Long> = ProtocolManager.ownProfileUpdated
|
||||||
|
|
||||||
|
override fun initialize(context: Context) {
|
||||||
|
ProtocolManager.bindDependencies(
|
||||||
|
messageRepository = messageRepository,
|
||||||
|
groupRepository = groupRepository,
|
||||||
|
accountManager = accountManager
|
||||||
|
)
|
||||||
|
ProtocolManager.initialize(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initializeAccount(publicKey: String, privateKey: String) =
|
||||||
|
ProtocolManager.initializeAccount(publicKey, privateKey)
|
||||||
|
|
||||||
|
override fun connect() = ProtocolManager.connect()
|
||||||
|
|
||||||
|
override fun authenticate(publicKey: String, privateHash: String) =
|
||||||
|
ProtocolManager.authenticate(publicKey, privateHash)
|
||||||
|
|
||||||
|
override fun reconnectNowIfNeeded(reason: String) = ProtocolManager.reconnectNowIfNeeded(reason)
|
||||||
|
|
||||||
|
override fun disconnect() = ProtocolManager.disconnect()
|
||||||
|
|
||||||
|
override fun isAuthenticated(): Boolean = ProtocolManager.isAuthenticated()
|
||||||
|
|
||||||
|
override fun getPrivateHash(): String? =
|
||||||
|
runCatching { ProtocolManager.getProtocol().getPrivateHash() }.getOrNull()
|
||||||
|
|
||||||
|
override fun subscribePushTokenIfAvailable(forceToken: String?) =
|
||||||
|
ProtocolManager.subscribePushTokenIfAvailable(forceToken)
|
||||||
|
|
||||||
|
override fun addLog(message: String) = ProtocolManager.addLog(message)
|
||||||
|
|
||||||
|
override fun enableUILogs(enabled: Boolean) = ProtocolManager.enableUILogs(enabled)
|
||||||
|
|
||||||
|
override fun clearLogs() = ProtocolManager.clearLogs()
|
||||||
|
|
||||||
|
override fun resolveOutgoingRetry(messageId: String) = ProtocolManager.resolveOutgoingRetry(messageId)
|
||||||
|
|
||||||
|
override fun getCachedUserByUsername(username: String): SearchUser? =
|
||||||
|
ProtocolManager.getCachedUserByUsername(username)
|
||||||
|
|
||||||
|
override fun getCachedUserName(publicKey: String): String? =
|
||||||
|
ProtocolManager.getCachedUserName(publicKey)
|
||||||
|
|
||||||
|
override fun getCachedUserInfo(publicKey: String): SearchUser? =
|
||||||
|
ProtocolManager.getCachedUserInfo(publicKey)
|
||||||
|
|
||||||
|
override fun acceptDevice(deviceId: String) = ProtocolManager.acceptDevice(deviceId)
|
||||||
|
|
||||||
|
override fun declineDevice(deviceId: String) = ProtocolManager.declineDevice(deviceId)
|
||||||
|
|
||||||
|
override fun send(packet: Packet) = ProtocolManager.send(packet)
|
||||||
|
|
||||||
|
override fun sendPacket(packet: Packet) = ProtocolManager.sendPacket(packet)
|
||||||
|
|
||||||
|
override fun sendMessageWithRetry(packet: PacketMessage) = ProtocolManager.sendMessageWithRetry(packet)
|
||||||
|
|
||||||
|
override fun waitPacket(packetId: Int, callback: (Packet) -> Unit) =
|
||||||
|
ProtocolManager.waitPacket(packetId, callback)
|
||||||
|
|
||||||
|
override fun unwaitPacket(packetId: Int, callback: (Packet) -> Unit) =
|
||||||
|
ProtocolManager.unwaitPacket(packetId, callback)
|
||||||
|
|
||||||
|
override fun packetFlow(packetId: Int): SharedFlow<Packet> = ProtocolManager.packetFlow(packetId)
|
||||||
|
|
||||||
|
override fun notifyOwnProfileUpdated() = ProtocolManager.notifyOwnProfileUpdated()
|
||||||
|
|
||||||
|
override fun restoreAuthFromStoredCredentials(
|
||||||
|
preferredPublicKey: String?,
|
||||||
|
reason: String
|
||||||
|
): Boolean = ProtocolManager.restoreAuthFromStoredCredentials(preferredPublicKey, reason)
|
||||||
|
|
||||||
|
override suspend fun resolveUserName(publicKey: String, timeoutMs: Long): String? =
|
||||||
|
ProtocolManager.resolveUserName(publicKey, timeoutMs)
|
||||||
|
|
||||||
|
override suspend fun resolveUserInfo(publicKey: String, timeoutMs: Long): SearchUser? =
|
||||||
|
ProtocolManager.resolveUserInfo(publicKey, timeoutMs)
|
||||||
|
|
||||||
|
override suspend fun searchUsers(query: String, timeoutMs: Long): List<SearchUser> =
|
||||||
|
ProtocolManager.searchUsers(query, timeoutMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class SessionCoordinatorImpl @Inject constructor(
|
||||||
|
private val accountManager: AccountManager,
|
||||||
|
private val protocolGateway: ProtocolGateway
|
||||||
|
) : SessionCoordinator {
|
||||||
|
override val sessionState: StateFlow<SessionState> = SessionStore.state
|
||||||
|
|
||||||
|
override fun dispatch(action: SessionAction) {
|
||||||
|
SessionStore.dispatch(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun bootstrapAuthenticatedSession(account: DecryptedAccount, reason: String) {
|
||||||
|
dispatch(SessionAction.AuthInProgress(publicKey = account.publicKey, reason = reason))
|
||||||
|
protocolGateway.initializeAccount(account.publicKey, account.privateKey)
|
||||||
|
protocolGateway.connect()
|
||||||
|
protocolGateway.authenticate(account.publicKey, account.privateKeyHash)
|
||||||
|
protocolGateway.reconnectNowIfNeeded("session_bootstrap_$reason")
|
||||||
|
accountManager.setCurrentAccount(account.publicKey)
|
||||||
|
dispatch(SessionAction.Ready(account = account, reason = reason))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class IdentityGatewayImpl @Inject constructor() : IdentityGateway {
|
||||||
|
override val state: StateFlow<IdentityStateSnapshot> = IdentityStore.state
|
||||||
|
|
||||||
|
override fun updateOwnProfile(
|
||||||
|
publicKey: String,
|
||||||
|
displayName: String?,
|
||||||
|
username: String?,
|
||||||
|
verified: Int?,
|
||||||
|
resolved: Boolean,
|
||||||
|
reason: String
|
||||||
|
) {
|
||||||
|
IdentityStore.updateOwnProfile(
|
||||||
|
publicKey = publicKey,
|
||||||
|
displayName = displayName,
|
||||||
|
username = username,
|
||||||
|
verified = verified,
|
||||||
|
resolved = resolved,
|
||||||
|
reason = reason
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object AppDataModule {
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideAccountManager(@ApplicationContext context: Context): AccountManager =
|
||||||
|
AccountManager(context)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providePreferencesManager(@ApplicationContext context: Context): PreferencesManager =
|
||||||
|
PreferencesManager(context)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
abstract class AppGatewayModule {
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindProtocolGateway(impl: ProtocolGatewayImpl): ProtocolGateway
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindSessionCoordinator(impl: SessionCoordinatorImpl): SessionCoordinator
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindIdentityGateway(impl: IdentityGatewayImpl): IdentityGateway
|
||||||
|
}
|
||||||
28
app/src/main/java/com/rosetta/messenger/di/UiEntryPoint.kt
Normal file
28
app/src/main/java/com/rosetta/messenger/di/UiEntryPoint.kt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.rosetta.messenger.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.rosetta.messenger.data.AccountManager
|
||||||
|
import com.rosetta.messenger.data.GroupRepository
|
||||||
|
import com.rosetta.messenger.data.MessageRepository
|
||||||
|
import com.rosetta.messenger.data.PreferencesManager
|
||||||
|
import dagger.hilt.EntryPoint
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@EntryPoint
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface UiEntryPoint {
|
||||||
|
fun protocolGateway(): ProtocolGateway
|
||||||
|
fun sessionCoordinator(): SessionCoordinator
|
||||||
|
fun identityGateway(): IdentityGateway
|
||||||
|
fun accountManager(): AccountManager
|
||||||
|
fun preferencesManager(): PreferencesManager
|
||||||
|
fun messageRepository(): MessageRepository
|
||||||
|
fun groupRepository(): GroupRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
object UiDependencyAccess {
|
||||||
|
fun get(context: Context): UiEntryPoint =
|
||||||
|
EntryPointAccessors.fromApplication(context.applicationContext, UiEntryPoint::class.java)
|
||||||
|
}
|
||||||
@@ -21,8 +21,11 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import com.rosetta.messenger.MainActivity
|
import com.rosetta.messenger.MainActivity
|
||||||
import com.rosetta.messenger.R
|
import com.rosetta.messenger.R
|
||||||
|
import com.rosetta.messenger.data.PreferencesManager
|
||||||
import com.rosetta.messenger.database.RosettaDatabase
|
import com.rosetta.messenger.database.RosettaDatabase
|
||||||
import com.rosetta.messenger.utils.AvatarFileManager
|
import com.rosetta.messenger.utils.AvatarFileManager
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -34,8 +37,11 @@ import kotlinx.coroutines.runBlocking
|
|||||||
* Keeps call alive while app goes to background.
|
* Keeps call alive while app goes to background.
|
||||||
* Uses Notification.CallStyle on Android 12+ and NotificationCompat fallback on older APIs.
|
* Uses Notification.CallStyle on Android 12+ and NotificationCompat fallback on older APIs.
|
||||||
*/
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
class CallForegroundService : Service() {
|
class CallForegroundService : Service() {
|
||||||
|
|
||||||
|
@Inject lateinit var preferencesManager: PreferencesManager
|
||||||
|
|
||||||
private data class Snapshot(
|
private data class Snapshot(
|
||||||
val phase: CallPhase,
|
val phase: CallPhase,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
@@ -469,8 +475,7 @@ class CallForegroundService : Service() {
|
|||||||
// Проверяем настройку
|
// Проверяем настройку
|
||||||
val avatarEnabled = runCatching {
|
val avatarEnabled = runCatching {
|
||||||
runBlocking(Dispatchers.IO) {
|
runBlocking(Dispatchers.IO) {
|
||||||
com.rosetta.messenger.data.PreferencesManager(applicationContext)
|
preferencesManager.notificationAvatarEnabled.first()
|
||||||
.notificationAvatarEnabled.first()
|
|
||||||
}
|
}
|
||||||
}.getOrDefault(true)
|
}.getOrDefault(true)
|
||||||
if (!avatarEnabled) return null
|
if (!avatarEnabled) return null
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.rosetta.messenger.BuildConfig
|
import com.rosetta.messenger.BuildConfig
|
||||||
|
import com.rosetta.messenger.data.AccountManager
|
||||||
import com.rosetta.messenger.data.MessageRepository
|
import com.rosetta.messenger.data.MessageRepository
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
@@ -111,6 +112,8 @@ object CallManager {
|
|||||||
@Volatile
|
@Volatile
|
||||||
private var initialized = false
|
private var initialized = false
|
||||||
private var appContext: Context? = null
|
private var appContext: Context? = null
|
||||||
|
private var messageRepository: MessageRepository? = null
|
||||||
|
private var accountManager: AccountManager? = null
|
||||||
private var ownPublicKey: String = ""
|
private var ownPublicKey: String = ""
|
||||||
|
|
||||||
private var role: CallRole? = null
|
private var role: CallRole? = null
|
||||||
@@ -213,6 +216,14 @@ object CallManager {
|
|||||||
ProtocolManager.requestIceServers()
|
ProtocolManager.requestIceServers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun bindDependencies(
|
||||||
|
messageRepository: MessageRepository,
|
||||||
|
accountManager: AccountManager
|
||||||
|
) {
|
||||||
|
this.messageRepository = messageRepository
|
||||||
|
this.accountManager = accountManager
|
||||||
|
}
|
||||||
|
|
||||||
fun bindAccount(publicKey: String) {
|
fun bindAccount(publicKey: String) {
|
||||||
ownPublicKey = publicKey.trim()
|
ownPublicKey = publicKey.trim()
|
||||||
}
|
}
|
||||||
@@ -318,7 +329,7 @@ object CallManager {
|
|||||||
|
|
||||||
// Если ownPublicKey пустой (push разбудил но аккаунт не привязан) — попробуем привязать
|
// Если ownPublicKey пустой (push разбудил но аккаунт не привязан) — попробуем привязать
|
||||||
if (ownPublicKey.isBlank()) {
|
if (ownPublicKey.isBlank()) {
|
||||||
val lastPk = appContext?.let { com.rosetta.messenger.data.AccountManager(it).getLastLoggedPublicKey() }.orEmpty()
|
val lastPk = accountManager?.getLastLoggedPublicKey().orEmpty()
|
||||||
if (lastPk.isNotBlank()) {
|
if (lastPk.isNotBlank()) {
|
||||||
bindAccount(lastPk)
|
bindAccount(lastPk)
|
||||||
breadcrumb("acceptIncomingCall: auto-bind account pk=${lastPk.take(8)}…")
|
breadcrumb("acceptIncomingCall: auto-bind account pk=${lastPk.take(8)}…")
|
||||||
@@ -1042,7 +1053,6 @@ object CallManager {
|
|||||||
|
|
||||||
private fun emitCallAttachmentIfNeeded(snapshot: CallUiState) {
|
private fun emitCallAttachmentIfNeeded(snapshot: CallUiState) {
|
||||||
val peerPublicKey = snapshot.peerPublicKey.trim()
|
val peerPublicKey = snapshot.peerPublicKey.trim()
|
||||||
val context = appContext ?: return
|
|
||||||
if (peerPublicKey.isBlank()) return
|
if (peerPublicKey.isBlank()) return
|
||||||
|
|
||||||
val durationSec = snapshot.durationSec.coerceAtLeast(0)
|
val durationSec = snapshot.durationSec.coerceAtLeast(0)
|
||||||
@@ -1061,9 +1071,14 @@ object CallManager {
|
|||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
|
val repository = messageRepository
|
||||||
|
if (repository == null) {
|
||||||
|
breadcrumb("CALL ATTACHMENT: MessageRepository not bound")
|
||||||
|
return@runCatching
|
||||||
|
}
|
||||||
if (capturedRole == CallRole.CALLER) {
|
if (capturedRole == CallRole.CALLER) {
|
||||||
// CALLER: send call attachment as a message (peer will receive it)
|
// CALLER: send call attachment as a message (peer will receive it)
|
||||||
MessageRepository.getInstance(context).sendMessage(
|
repository.sendMessage(
|
||||||
toPublicKey = peerPublicKey,
|
toPublicKey = peerPublicKey,
|
||||||
text = "",
|
text = "",
|
||||||
attachments = listOf(callAttachment)
|
attachments = listOf(callAttachment)
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ object ProtocolManager {
|
|||||||
@Volatile private var protocol: Protocol? = null
|
@Volatile private var protocol: Protocol? = null
|
||||||
private var messageRepository: MessageRepository? = null
|
private var messageRepository: MessageRepository? = null
|
||||||
private var groupRepository: GroupRepository? = null
|
private var groupRepository: GroupRepository? = null
|
||||||
|
private var accountManager: AccountManager? = null
|
||||||
private var appContext: Context? = null
|
private var appContext: Context? = null
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
private val protocolInstanceLock = Any()
|
private val protocolInstanceLock = Any()
|
||||||
@@ -100,10 +101,22 @@ object ProtocolManager {
|
|||||||
waitForNetworkAndReconnect = ::waitForNetworkAndReconnect,
|
waitForNetworkAndReconnect = ::waitForNetworkAndReconnect,
|
||||||
stopWaitingForNetwork = { reason -> stopWaitingForNetwork(reason) },
|
stopWaitingForNetwork = { reason -> stopWaitingForNetwork(reason) },
|
||||||
getProtocol = ::getProtocol,
|
getProtocol = ::getProtocol,
|
||||||
appContextProvider = { appContext },
|
persistHandshakeCredentials = { publicKey, privateHash ->
|
||||||
|
accountManager?.setLastLoggedPublicKey(publicKey)
|
||||||
|
accountManager?.setLastLoggedPrivateKeyHash(privateHash)
|
||||||
|
},
|
||||||
buildHandshakeDevice = ::buildHandshakeDevice
|
buildHandshakeDevice = ::buildHandshakeDevice
|
||||||
)
|
)
|
||||||
private val ownProfileSyncService = OwnProfileSyncService(::isPlaceholderAccountName)
|
private val ownProfileSyncService =
|
||||||
|
OwnProfileSyncService(
|
||||||
|
isPlaceholderAccountName = ::isPlaceholderAccountName,
|
||||||
|
updateAccountName = { publicKey, name ->
|
||||||
|
accountManager?.updateAccountName(publicKey, name)
|
||||||
|
},
|
||||||
|
updateAccountUsername = { publicKey, username ->
|
||||||
|
accountManager?.updateAccountUsername(publicKey, username)
|
||||||
|
}
|
||||||
|
)
|
||||||
private val packetRouter by lazy {
|
private val packetRouter by lazy {
|
||||||
PacketRouter(
|
PacketRouter(
|
||||||
sendSearchPacket = { packet -> send(packet) },
|
sendSearchPacket = { packet -> send(packet) },
|
||||||
@@ -239,10 +252,12 @@ object ProtocolManager {
|
|||||||
)
|
)
|
||||||
setSyncInProgress(false)
|
setSyncInProgress(false)
|
||||||
clearTypingState()
|
clearTypingState()
|
||||||
if (messageRepository == null) {
|
val repository = messageRepository
|
||||||
appContext?.let { messageRepository = MessageRepository.getInstance(it) }
|
if (repository == null) {
|
||||||
|
addLog("❌ initializeAccount aborted: MessageRepository is not bound")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
messageRepository?.initialize(normalizedPublicKey, normalizedPrivateKey)
|
repository.initialize(normalizedPublicKey, normalizedPrivateKey)
|
||||||
|
|
||||||
val sameAccount =
|
val sameAccount =
|
||||||
bootstrapContext.accountPublicKey.equals(normalizedPublicKey, ignoreCase = true)
|
bootstrapContext.accountPublicKey.equals(normalizedPublicKey, ignoreCase = true)
|
||||||
@@ -601,14 +616,39 @@ object ProtocolManager {
|
|||||||
addLog("⚠️ $reason")
|
addLog("⚠️ $reason")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject process-wide dependencies from DI container.
|
||||||
|
*/
|
||||||
|
fun bindDependencies(
|
||||||
|
messageRepository: MessageRepository,
|
||||||
|
groupRepository: GroupRepository,
|
||||||
|
accountManager: AccountManager
|
||||||
|
) {
|
||||||
|
this.messageRepository = messageRepository
|
||||||
|
this.groupRepository = groupRepository
|
||||||
|
this.accountManager = accountManager
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backward-compatible alias kept while migrating call sites.
|
||||||
|
*/
|
||||||
|
fun bindRepositories(
|
||||||
|
messageRepository: MessageRepository,
|
||||||
|
groupRepository: GroupRepository
|
||||||
|
) {
|
||||||
|
this.messageRepository = messageRepository
|
||||||
|
this.groupRepository = groupRepository
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Инициализация с контекстом для доступа к MessageRepository
|
* Инициализация с контекстом для доступа к MessageRepository
|
||||||
*/
|
*/
|
||||||
fun initialize(context: Context) {
|
fun initialize(context: Context) {
|
||||||
appContext = context.applicationContext
|
appContext = context.applicationContext
|
||||||
messageRepository = MessageRepository.getInstance(context)
|
if (messageRepository == null || groupRepository == null || accountManager == null) {
|
||||||
groupRepository = GroupRepository.getInstance(context)
|
addLog("⚠️ initialize called before dependencies were bound via DI")
|
||||||
|
}
|
||||||
ensureConnectionSupervisor()
|
ensureConnectionSupervisor()
|
||||||
if (!packetHandlersRegistered) {
|
if (!packetHandlersRegistered) {
|
||||||
setupPacketHandlers()
|
setupPacketHandlers()
|
||||||
@@ -854,7 +894,6 @@ object ProtocolManager {
|
|||||||
|
|
||||||
val ownProfileResolved =
|
val ownProfileResolved =
|
||||||
ownProfileSyncService.applyOwnProfileFromSearch(
|
ownProfileSyncService.applyOwnProfileFromSearch(
|
||||||
appContext = appContext,
|
|
||||||
ownPublicKey = ownPublicKey,
|
ownPublicKey = ownPublicKey,
|
||||||
user = user
|
user = user
|
||||||
)
|
)
|
||||||
@@ -1537,8 +1576,11 @@ object ProtocolManager {
|
|||||||
preferredPublicKey: String? = null,
|
preferredPublicKey: String? = null,
|
||||||
reason: String = "background_restore"
|
reason: String = "background_restore"
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val context = appContext ?: return false
|
val accountManager = accountManager
|
||||||
val accountManager = AccountManager(context)
|
if (accountManager == null) {
|
||||||
|
addLog("⚠️ restoreAuthFromStoredCredentials skipped: AccountManager is not bound")
|
||||||
|
return false
|
||||||
|
}
|
||||||
val publicKey =
|
val publicKey =
|
||||||
preferredPublicKey?.trim().orEmpty().ifBlank {
|
preferredPublicKey?.trim().orEmpty().ifBlank {
|
||||||
accountManager.getLastLoggedPublicKey().orEmpty()
|
accountManager.getLastLoggedPublicKey().orEmpty()
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.rosetta.messenger.network.connection
|
package com.rosetta.messenger.network.connection
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.rosetta.messenger.data.AccountManager
|
|
||||||
import com.rosetta.messenger.network.HandshakeDevice
|
import com.rosetta.messenger.network.HandshakeDevice
|
||||||
import com.rosetta.messenger.network.Protocol
|
import com.rosetta.messenger.network.Protocol
|
||||||
|
|
||||||
@@ -10,7 +8,7 @@ class ConnectionOrchestrator(
|
|||||||
private val waitForNetworkAndReconnect: (String) -> Unit,
|
private val waitForNetworkAndReconnect: (String) -> Unit,
|
||||||
private val stopWaitingForNetwork: (String) -> Unit,
|
private val stopWaitingForNetwork: (String) -> Unit,
|
||||||
private val getProtocol: () -> Protocol,
|
private val getProtocol: () -> Protocol,
|
||||||
private val appContextProvider: () -> Context?,
|
private val persistHandshakeCredentials: (publicKey: String, privateHash: String) -> Unit,
|
||||||
private val buildHandshakeDevice: () -> HandshakeDevice
|
private val buildHandshakeDevice: () -> HandshakeDevice
|
||||||
) {
|
) {
|
||||||
fun handleConnect(reason: String) {
|
fun handleConnect(reason: String) {
|
||||||
@@ -32,13 +30,7 @@ class ConnectionOrchestrator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun handleAuthenticate(publicKey: String, privateHash: String) {
|
fun handleAuthenticate(publicKey: String, privateHash: String) {
|
||||||
appContextProvider()?.let { context ->
|
runCatching { persistHandshakeCredentials(publicKey, privateHash) }
|
||||||
runCatching {
|
|
||||||
val accountManager = AccountManager(context)
|
|
||||||
accountManager.setLastLoggedPublicKey(publicKey)
|
|
||||||
accountManager.setLastLoggedPrivateKeyHash(privateHash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val device = buildHandshakeDevice()
|
val device = buildHandshakeDevice()
|
||||||
getProtocol().startHandshake(publicKey, privateHash, device)
|
getProtocol().startHandshake(publicKey, privateHash, device)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.rosetta.messenger.network.connection
|
package com.rosetta.messenger.network.connection
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.rosetta.messenger.data.AccountManager
|
|
||||||
import com.rosetta.messenger.network.PacketSearch
|
import com.rosetta.messenger.network.PacketSearch
|
||||||
import com.rosetta.messenger.network.SearchUser
|
import com.rosetta.messenger.network.SearchUser
|
||||||
import com.rosetta.messenger.session.IdentityStore
|
import com.rosetta.messenger.session.IdentityStore
|
||||||
@@ -10,7 +8,9 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
class OwnProfileSyncService(
|
class OwnProfileSyncService(
|
||||||
private val isPlaceholderAccountName: (String?) -> Boolean
|
private val isPlaceholderAccountName: (String?) -> Boolean,
|
||||||
|
private val updateAccountName: suspend (publicKey: String, name: String) -> Unit,
|
||||||
|
private val updateAccountUsername: suspend (publicKey: String, username: String) -> Unit
|
||||||
) {
|
) {
|
||||||
private val _ownProfileUpdated = MutableStateFlow(0L)
|
private val _ownProfileUpdated = MutableStateFlow(0L)
|
||||||
val ownProfileUpdated: StateFlow<Long> = _ownProfileUpdated.asStateFlow()
|
val ownProfileUpdated: StateFlow<Long> = _ownProfileUpdated.asStateFlow()
|
||||||
@@ -20,20 +20,17 @@ class OwnProfileSyncService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun applyOwnProfileFromSearch(
|
suspend fun applyOwnProfileFromSearch(
|
||||||
appContext: Context?,
|
|
||||||
ownPublicKey: String,
|
ownPublicKey: String,
|
||||||
user: SearchUser
|
user: SearchUser
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (ownPublicKey.isBlank()) return false
|
if (ownPublicKey.isBlank()) return false
|
||||||
if (!user.publicKey.equals(ownPublicKey, ignoreCase = true)) return false
|
if (!user.publicKey.equals(ownPublicKey, ignoreCase = true)) return false
|
||||||
|
|
||||||
val context = appContext ?: return true
|
|
||||||
val accountManager = AccountManager(context)
|
|
||||||
if (user.title.isNotBlank() && !isPlaceholderAccountName(user.title)) {
|
if (user.title.isNotBlank() && !isPlaceholderAccountName(user.title)) {
|
||||||
accountManager.updateAccountName(ownPublicKey, user.title)
|
updateAccountName(ownPublicKey, user.title)
|
||||||
}
|
}
|
||||||
if (user.username.isNotBlank()) {
|
if (user.username.isNotBlank()) {
|
||||||
accountManager.updateAccountUsername(ownPublicKey, user.username)
|
updateAccountUsername(ownPublicKey, user.username)
|
||||||
}
|
}
|
||||||
IdentityStore.updateOwnProfile(user, reason = "protocol_search_own_profile")
|
IdentityStore.updateOwnProfile(user, reason = "protocol_search_own_profile")
|
||||||
_ownProfileUpdated.value = System.currentTimeMillis()
|
_ownProfileUpdated.value = System.currentTimeMillis()
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ import com.rosetta.messenger.R
|
|||||||
import com.rosetta.messenger.data.AccountManager
|
import com.rosetta.messenger.data.AccountManager
|
||||||
import com.rosetta.messenger.data.PreferencesManager
|
import com.rosetta.messenger.data.PreferencesManager
|
||||||
import com.rosetta.messenger.database.RosettaDatabase
|
import com.rosetta.messenger.database.RosettaDatabase
|
||||||
|
import com.rosetta.messenger.di.ProtocolGateway
|
||||||
import com.rosetta.messenger.network.CallForegroundService
|
import com.rosetta.messenger.network.CallForegroundService
|
||||||
import com.rosetta.messenger.network.CallManager
|
import com.rosetta.messenger.network.CallManager
|
||||||
import com.rosetta.messenger.network.CallPhase
|
import com.rosetta.messenger.network.CallPhase
|
||||||
import com.rosetta.messenger.network.CallUiState
|
import com.rosetta.messenger.network.CallUiState
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
|
||||||
import com.rosetta.messenger.utils.AvatarFileManager
|
import com.rosetta.messenger.utils.AvatarFileManager
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -40,8 +42,13 @@ import java.util.Locale
|
|||||||
* - Получение push-уведомлений о новых сообщениях
|
* - Получение push-уведомлений о новых сообщениях
|
||||||
* - Отображение уведомлений
|
* - Отображение уведомлений
|
||||||
*/
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
|
|
||||||
|
@Inject lateinit var accountManager: AccountManager
|
||||||
|
@Inject lateinit var preferencesManager: PreferencesManager
|
||||||
|
@Inject lateinit var protocolGateway: ProtocolGateway
|
||||||
|
|
||||||
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -121,8 +128,8 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
|
|
||||||
// Best-effort: если соединение уже авторизовано — сразу обновляем подписку на push.
|
// Best-effort: если соединение уже авторизовано — сразу обновляем подписку на push.
|
||||||
// Используем единую точку отправки в ProtocolManager (с дедупликацией).
|
// Используем единую точку отправки в ProtocolManager (с дедупликацией).
|
||||||
if (ProtocolManager.isAuthenticated()) {
|
if (protocolGateway.isAuthenticated()) {
|
||||||
runCatching { ProtocolManager.subscribePushTokenIfAvailable(forceToken = token) }
|
runCatching { protocolGateway.subscribePushTokenIfAvailable(forceToken = token) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +155,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
if (!hasDataContent && !hasNotificationContent) {
|
if (!hasDataContent && !hasNotificationContent) {
|
||||||
Log.d(TAG, "Silent/empty push ignored (iOS wake-up push)")
|
Log.d(TAG, "Silent/empty push ignored (iOS wake-up push)")
|
||||||
// Still trigger reconnect if WebSocket is disconnected
|
// Still trigger reconnect if WebSocket is disconnected
|
||||||
com.rosetta.messenger.network.ProtocolManager.reconnectNowIfNeeded("silent_push")
|
protocolGateway.reconnectNowIfNeeded("silent_push")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,18 +521,18 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
/** Пробуждаем сетевой слой по high-priority push, чтобы быстрее получить сигнальные пакеты */
|
/** Пробуждаем сетевой слой по high-priority push, чтобы быстрее получить сигнальные пакеты */
|
||||||
private fun wakeProtocolFromPush(reason: String) {
|
private fun wakeProtocolFromPush(reason: String) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val account = AccountManager(applicationContext).getLastLoggedPublicKey().orEmpty()
|
val account = accountManager.getLastLoggedPublicKey().orEmpty()
|
||||||
ProtocolManager.initialize(applicationContext)
|
protocolGateway.initialize(applicationContext)
|
||||||
CallManager.initialize(applicationContext)
|
CallManager.initialize(applicationContext)
|
||||||
if (account.isNotBlank()) {
|
if (account.isNotBlank()) {
|
||||||
CallManager.bindAccount(account)
|
CallManager.bindAccount(account)
|
||||||
}
|
}
|
||||||
val restored = ProtocolManager.restoreAuthFromStoredCredentials(
|
val restored = protocolGateway.restoreAuthFromStoredCredentials(
|
||||||
preferredPublicKey = account,
|
preferredPublicKey = account,
|
||||||
reason = "push_$reason"
|
reason = "push_$reason"
|
||||||
)
|
)
|
||||||
pushCallLog("wakeProtocolFromPush: authRestore=$restored account=${account.take(8)}…")
|
pushCallLog("wakeProtocolFromPush: authRestore=$restored account=${account.take(8)}…")
|
||||||
ProtocolManager.reconnectNowIfNeeded("push_$reason")
|
protocolGateway.reconnectNowIfNeeded("push_$reason")
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
Log.w(TAG, "wakeProtocolFromPush failed: ${error.message}")
|
Log.w(TAG, "wakeProtocolFromPush failed: ${error.message}")
|
||||||
}
|
}
|
||||||
@@ -560,7 +567,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
private fun areNotificationsEnabled(): Boolean {
|
private fun areNotificationsEnabled(): Boolean {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
runBlocking(Dispatchers.IO) {
|
runBlocking(Dispatchers.IO) {
|
||||||
PreferencesManager(applicationContext).notificationsEnabled.first()
|
preferencesManager.notificationsEnabled.first()
|
||||||
}
|
}
|
||||||
}.getOrDefault(true)
|
}.getOrDefault(true)
|
||||||
}
|
}
|
||||||
@@ -583,7 +590,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
parsedDialogKey: String?,
|
parsedDialogKey: String?,
|
||||||
parsedSenderKey: String?
|
parsedSenderKey: String?
|
||||||
): Set<String> {
|
): Set<String> {
|
||||||
val currentAccount = AccountManager(applicationContext).getLastLoggedPublicKey().orEmpty().trim()
|
val currentAccount = accountManager.getLastLoggedPublicKey().orEmpty().trim()
|
||||||
val candidates = linkedSetOf<String>()
|
val candidates = linkedSetOf<String>()
|
||||||
|
|
||||||
fun addCandidate(raw: String?) {
|
fun addCandidate(raw: String?) {
|
||||||
@@ -726,7 +733,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
private fun isAvatarInNotificationsEnabled(): Boolean {
|
private fun isAvatarInNotificationsEnabled(): Boolean {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
runBlocking(Dispatchers.IO) {
|
runBlocking(Dispatchers.IO) {
|
||||||
PreferencesManager(applicationContext).notificationAvatarEnabled.first()
|
preferencesManager.notificationAvatarEnabled.first()
|
||||||
}
|
}
|
||||||
}.getOrDefault(true)
|
}.getOrDefault(true)
|
||||||
}
|
}
|
||||||
@@ -735,12 +742,10 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
private fun isDialogMuted(senderPublicKey: String): Boolean {
|
private fun isDialogMuted(senderPublicKey: String): Boolean {
|
||||||
if (senderPublicKey.isBlank()) return false
|
if (senderPublicKey.isBlank()) return false
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val accountManager = AccountManager(applicationContext)
|
|
||||||
val currentAccount = accountManager.getLastLoggedPublicKey().orEmpty()
|
val currentAccount = accountManager.getLastLoggedPublicKey().orEmpty()
|
||||||
runBlocking(Dispatchers.IO) {
|
runBlocking(Dispatchers.IO) {
|
||||||
val preferences = PreferencesManager(applicationContext)
|
|
||||||
buildDialogKeyVariants(senderPublicKey).any { key ->
|
buildDialogKeyVariants(senderPublicKey).any { key ->
|
||||||
preferences.isChatMuted(currentAccount, key)
|
preferencesManager.isChatMuted(currentAccount, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.getOrDefault(false)
|
}.getOrDefault(false)
|
||||||
@@ -750,10 +755,10 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
private fun resolveNameForKey(publicKey: String?): String? {
|
private fun resolveNameForKey(publicKey: String?): String? {
|
||||||
if (publicKey.isNullOrBlank()) return null
|
if (publicKey.isNullOrBlank()) return null
|
||||||
// 1. In-memory cache
|
// 1. In-memory cache
|
||||||
ProtocolManager.getCachedUserName(publicKey)?.let { return it }
|
protocolGateway.getCachedUserName(publicKey)?.let { return it }
|
||||||
// 2. DB dialogs table
|
// 2. DB dialogs table
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val account = AccountManager(applicationContext).getLastLoggedPublicKey().orEmpty()
|
val account = accountManager.getLastLoggedPublicKey().orEmpty()
|
||||||
if (account.isBlank()) return null
|
if (account.isBlank()) return null
|
||||||
val db = RosettaDatabase.getDatabase(applicationContext)
|
val db = RosettaDatabase.getDatabase(applicationContext)
|
||||||
val dialog = runBlocking(Dispatchers.IO) {
|
val dialog = runBlocking(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
package com.rosetta.messenger.session
|
package com.rosetta.messenger.session
|
||||||
|
|
||||||
import com.rosetta.messenger.data.AccountManager
|
|
||||||
import com.rosetta.messenger.data.DecryptedAccount
|
import com.rosetta.messenger.data.DecryptedAccount
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
|
|
||||||
sealed interface SessionState {
|
sealed interface SessionState {
|
||||||
data object LoggedOut : SessionState
|
data object LoggedOut : SessionState
|
||||||
@@ -24,55 +20,30 @@ sealed interface SessionState {
|
|||||||
* UI should rely on this state instead of scattering account checks.
|
* UI should rely on this state instead of scattering account checks.
|
||||||
*/
|
*/
|
||||||
object AppSessionCoordinator {
|
object AppSessionCoordinator {
|
||||||
private val _sessionState = MutableStateFlow<SessionState>(SessionState.LoggedOut)
|
val sessionState: StateFlow<SessionState> = SessionStore.state
|
||||||
val sessionState: StateFlow<SessionState> = _sessionState.asStateFlow()
|
|
||||||
|
fun dispatch(action: SessionAction) {
|
||||||
|
SessionStore.dispatch(action)
|
||||||
|
}
|
||||||
|
|
||||||
fun markLoggedOut(reason: String = "") {
|
fun markLoggedOut(reason: String = "") {
|
||||||
_sessionState.value = SessionState.LoggedOut
|
dispatch(SessionAction.LoggedOut(reason = reason))
|
||||||
IdentityStore.markLoggedOut(reason = reason)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markAuthInProgress(publicKey: String? = null, reason: String = "") {
|
fun markAuthInProgress(publicKey: String? = null, reason: String = "") {
|
||||||
_sessionState.value = SessionState.AuthInProgress(publicKey = publicKey, reason = reason)
|
dispatch(
|
||||||
IdentityStore.markAuthInProgress(publicKey = publicKey, reason = reason)
|
SessionAction.AuthInProgress(
|
||||||
|
publicKey = publicKey,
|
||||||
|
reason = reason
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markReady(account: DecryptedAccount, reason: String = "") {
|
fun markReady(account: DecryptedAccount, reason: String = "") {
|
||||||
_sessionState.value = SessionState.Ready(account = account, reason = reason)
|
dispatch(SessionAction.Ready(account = account, reason = reason))
|
||||||
IdentityStore.setAccount(account = account, reason = reason)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun syncFromCachedAccount(account: DecryptedAccount?) {
|
fun syncFromCachedAccount(account: DecryptedAccount?) {
|
||||||
if (account == null) {
|
dispatch(SessionAction.SyncFromCachedAccount(account = account))
|
||||||
if (_sessionState.value is SessionState.Ready) {
|
|
||||||
_sessionState.value = SessionState.LoggedOut
|
|
||||||
}
|
|
||||||
IdentityStore.markLoggedOut(reason = "cached_account_cleared")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_sessionState.value = SessionState.Ready(account = account, reason = "cached")
|
|
||||||
IdentityStore.setAccount(account = account, reason = "cached")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unified bootstrap used by registration and unlock flows.
|
|
||||||
* Keeps protocol/account initialization sequence in one place.
|
|
||||||
*/
|
|
||||||
suspend fun bootstrapAuthenticatedSession(
|
|
||||||
accountManager: AccountManager,
|
|
||||||
account: DecryptedAccount,
|
|
||||||
reason: String
|
|
||||||
) {
|
|
||||||
markAuthInProgress(publicKey = account.publicKey, reason = reason)
|
|
||||||
|
|
||||||
// Initialize storage-bound account context before handshake completes
|
|
||||||
// to avoid early sync/message race conditions.
|
|
||||||
ProtocolManager.initializeAccount(account.publicKey, account.privateKey)
|
|
||||||
ProtocolManager.connect()
|
|
||||||
ProtocolManager.authenticate(account.publicKey, account.privateKeyHash)
|
|
||||||
ProtocolManager.reconnectNowIfNeeded("session_bootstrap_$reason")
|
|
||||||
|
|
||||||
accountManager.setCurrentAccount(account.publicKey)
|
|
||||||
markReady(account, reason = reason)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.rosetta.messenger.session
|
||||||
|
|
||||||
|
import com.rosetta.messenger.data.DecryptedAccount
|
||||||
|
|
||||||
|
sealed interface SessionAction {
|
||||||
|
data class LoggedOut(val reason: String = "") : SessionAction
|
||||||
|
|
||||||
|
data class AuthInProgress(
|
||||||
|
val publicKey: String? = null,
|
||||||
|
val reason: String = ""
|
||||||
|
) : SessionAction
|
||||||
|
|
||||||
|
data class Ready(
|
||||||
|
val account: DecryptedAccount,
|
||||||
|
val reason: String = ""
|
||||||
|
) : SessionAction
|
||||||
|
|
||||||
|
data class SyncFromCachedAccount(val account: DecryptedAccount?) : SessionAction
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.rosetta.messenger.session
|
||||||
|
|
||||||
|
object SessionReducer {
|
||||||
|
fun reduce(current: SessionState, action: SessionAction): SessionState {
|
||||||
|
return when (action) {
|
||||||
|
is SessionAction.LoggedOut -> SessionState.LoggedOut
|
||||||
|
is SessionAction.AuthInProgress ->
|
||||||
|
SessionState.AuthInProgress(
|
||||||
|
publicKey = action.publicKey?.trim().orEmpty().ifBlank { null },
|
||||||
|
reason = action.reason
|
||||||
|
)
|
||||||
|
is SessionAction.Ready ->
|
||||||
|
SessionState.Ready(
|
||||||
|
account = action.account,
|
||||||
|
reason = action.reason
|
||||||
|
)
|
||||||
|
is SessionAction.SyncFromCachedAccount -> {
|
||||||
|
val account = action.account
|
||||||
|
if (account == null) {
|
||||||
|
if (current is SessionState.Ready) SessionState.LoggedOut else current
|
||||||
|
} else {
|
||||||
|
SessionState.Ready(account = account, reason = "cached")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.rosetta.messenger.session
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single runtime source of truth for session lifecycle state.
|
||||||
|
* State transitions are produced only by SessionReducer.
|
||||||
|
*/
|
||||||
|
object SessionStore {
|
||||||
|
private val _state = MutableStateFlow<SessionState>(SessionState.LoggedOut)
|
||||||
|
val state: StateFlow<SessionState> = _state.asStateFlow()
|
||||||
|
|
||||||
|
private val lock = Any()
|
||||||
|
|
||||||
|
fun dispatch(action: SessionAction) {
|
||||||
|
synchronized(lock) {
|
||||||
|
_state.value = SessionReducer.reduce(_state.value, action)
|
||||||
|
}
|
||||||
|
syncIdentity(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncIdentity(action: SessionAction) {
|
||||||
|
when (action) {
|
||||||
|
is SessionAction.LoggedOut -> {
|
||||||
|
IdentityStore.markLoggedOut(reason = action.reason)
|
||||||
|
}
|
||||||
|
is SessionAction.AuthInProgress -> {
|
||||||
|
IdentityStore.markAuthInProgress(
|
||||||
|
publicKey = action.publicKey,
|
||||||
|
reason = action.reason
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is SessionAction.Ready -> {
|
||||||
|
IdentityStore.setAccount(
|
||||||
|
account = action.account,
|
||||||
|
reason = action.reason
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is SessionAction.SyncFromCachedAccount -> {
|
||||||
|
val account = action.account
|
||||||
|
if (account == null) {
|
||||||
|
IdentityStore.markLoggedOut(reason = "cached_account_cleared")
|
||||||
|
} else {
|
||||||
|
IdentityStore.setAccount(account = account, reason = "cached")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import androidx.compose.ui.platform.LocalView
|
|||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import com.rosetta.messenger.data.DecryptedAccount
|
import com.rosetta.messenger.data.DecryptedAccount
|
||||||
import com.rosetta.messenger.data.AccountManager
|
import com.rosetta.messenger.data.AccountManager
|
||||||
import com.rosetta.messenger.session.AppSessionCoordinator
|
import com.rosetta.messenger.di.SessionCoordinator
|
||||||
|
|
||||||
enum class AuthScreen {
|
enum class AuthScreen {
|
||||||
SELECT_ACCOUNT,
|
SELECT_ACCOUNT,
|
||||||
@@ -28,6 +28,7 @@ fun AuthFlow(
|
|||||||
hasExistingAccount: Boolean,
|
hasExistingAccount: Boolean,
|
||||||
accounts: List<AccountInfo> = emptyList(),
|
accounts: List<AccountInfo> = emptyList(),
|
||||||
accountManager: AccountManager,
|
accountManager: AccountManager,
|
||||||
|
sessionCoordinator: SessionCoordinator,
|
||||||
startInCreateMode: Boolean = false,
|
startInCreateMode: Boolean = false,
|
||||||
onAuthComplete: (DecryptedAccount?) -> Unit,
|
onAuthComplete: (DecryptedAccount?) -> Unit,
|
||||||
onLogout: () -> Unit = {}
|
onLogout: () -> Unit = {}
|
||||||
@@ -64,7 +65,7 @@ fun AuthFlow(
|
|||||||
var isImportMode by remember { mutableStateOf(false) }
|
var isImportMode by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(currentScreen, selectedAccountId) {
|
LaunchedEffect(currentScreen, selectedAccountId) {
|
||||||
AppSessionCoordinator.markAuthInProgress(
|
sessionCoordinator.markAuthInProgress(
|
||||||
publicKey = selectedAccountId,
|
publicKey = selectedAccountId,
|
||||||
reason = "auth_flow_${currentScreen.name.lowercase()}"
|
reason = "auth_flow_${currentScreen.name.lowercase()}"
|
||||||
)
|
)
|
||||||
@@ -177,6 +178,8 @@ fun AuthFlow(
|
|||||||
seedPhrase = seedPhrase,
|
seedPhrase = seedPhrase,
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
isImportMode = isImportMode,
|
isImportMode = isImportMode,
|
||||||
|
accountManager = accountManager,
|
||||||
|
sessionCoordinator = sessionCoordinator,
|
||||||
onBack = {
|
onBack = {
|
||||||
if (isImportMode) {
|
if (isImportMode) {
|
||||||
currentScreen = AuthScreen.IMPORT_SEED
|
currentScreen = AuthScreen.IMPORT_SEED
|
||||||
@@ -236,6 +239,8 @@ fun AuthFlow(
|
|||||||
UnlockScreen(
|
UnlockScreen(
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
selectedAccountId = selectedAccountId,
|
selectedAccountId = selectedAccountId,
|
||||||
|
accountManager = accountManager,
|
||||||
|
sessionCoordinator = sessionCoordinator,
|
||||||
onUnlocked = { account -> onAuthComplete(account) },
|
onUnlocked = { account -> onAuthComplete(account) },
|
||||||
onSwitchAccount = {
|
onSwitchAccount = {
|
||||||
// Navigate to create new account screen
|
// Navigate to create new account screen
|
||||||
|
|||||||
@@ -1,28 +1,33 @@
|
|||||||
package com.rosetta.messenger.ui.auth
|
package com.rosetta.messenger.ui.auth
|
||||||
|
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
import com.rosetta.messenger.di.ProtocolGateway
|
||||||
import com.rosetta.messenger.network.ProtocolState
|
import com.rosetta.messenger.network.ProtocolState
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
|
||||||
internal fun startAuthHandshakeFast(publicKey: String, privateKeyHash: String) {
|
internal fun startAuthHandshakeFast(
|
||||||
|
protocolGateway: ProtocolGateway,
|
||||||
|
publicKey: String,
|
||||||
|
privateKeyHash: String
|
||||||
|
) {
|
||||||
// Desktop parity: start connection+handshake immediately, without artificial waits.
|
// Desktop parity: start connection+handshake immediately, without artificial waits.
|
||||||
ProtocolManager.connect()
|
protocolGateway.connect()
|
||||||
ProtocolManager.authenticate(publicKey, privateKeyHash)
|
protocolGateway.authenticate(publicKey, privateKeyHash)
|
||||||
ProtocolManager.reconnectNowIfNeeded("auth_fast_start")
|
protocolGateway.reconnectNowIfNeeded("auth_fast_start")
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun awaitAuthHandshakeState(
|
internal suspend fun awaitAuthHandshakeState(
|
||||||
|
protocolGateway: ProtocolGateway,
|
||||||
publicKey: String,
|
publicKey: String,
|
||||||
privateKeyHash: String,
|
privateKeyHash: String,
|
||||||
attempts: Int = 2,
|
attempts: Int = 2,
|
||||||
timeoutMs: Long = 25_000L
|
timeoutMs: Long = 25_000L
|
||||||
): ProtocolState? {
|
): ProtocolState? {
|
||||||
repeat(attempts) { attempt ->
|
repeat(attempts) { attempt ->
|
||||||
startAuthHandshakeFast(publicKey, privateKeyHash)
|
startAuthHandshakeFast(protocolGateway, publicKey, privateKeyHash)
|
||||||
|
|
||||||
val state = withTimeoutOrNull(timeoutMs) {
|
val state = withTimeoutOrNull(timeoutMs) {
|
||||||
ProtocolManager.state.first {
|
protocolGateway.state.first {
|
||||||
it == ProtocolState.AUTHENTICATED ||
|
it == ProtocolState.AUTHENTICATED ||
|
||||||
it == ProtocolState.DEVICE_VERIFICATION_REQUIRED
|
it == ProtocolState.DEVICE_VERIFICATION_REQUIRED
|
||||||
}
|
}
|
||||||
@@ -30,7 +35,7 @@ internal suspend fun awaitAuthHandshakeState(
|
|||||||
if (state != null) {
|
if (state != null) {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
ProtocolManager.reconnectNowIfNeeded("auth_handshake_retry_${attempt + 1}")
|
protocolGateway.reconnectNowIfNeeded("auth_handshake_retry_${attempt + 1}")
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -52,10 +53,10 @@ import com.airbnb.lottie.compose.LottieConstants
|
|||||||
import com.airbnb.lottie.compose.animateLottieCompositionAsState
|
import com.airbnb.lottie.compose.animateLottieCompositionAsState
|
||||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||||
import com.rosetta.messenger.R
|
import com.rosetta.messenger.R
|
||||||
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.network.DeviceResolveSolution
|
import com.rosetta.messenger.network.DeviceResolveSolution
|
||||||
import com.rosetta.messenger.network.Packet
|
import com.rosetta.messenger.network.Packet
|
||||||
import com.rosetta.messenger.network.PacketDeviceResolve
|
import com.rosetta.messenger.network.PacketDeviceResolve
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
import compose.icons.TablerIcons
|
import compose.icons.TablerIcons
|
||||||
import compose.icons.tablericons.DeviceMobile
|
import compose.icons.tablericons.DeviceMobile
|
||||||
@@ -66,6 +67,9 @@ fun DeviceConfirmScreen(
|
|||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
onExit: () -> Unit
|
onExit: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val protocolGateway = remember(uiDeps) { uiDeps.protocolGateway() }
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
if (!view.isInEditMode) {
|
if (!view.isInEditMode) {
|
||||||
SideEffect {
|
SideEffect {
|
||||||
@@ -131,9 +135,9 @@ fun DeviceConfirmScreen(
|
|||||||
scope.launch { onExitState() }
|
scope.launch { onExitState() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProtocolManager.waitPacket(0x18, callback)
|
protocolGateway.waitPacket(0x18, callback)
|
||||||
onDispose {
|
onDispose {
|
||||||
ProtocolManager.unwaitPacket(0x18, callback)
|
protocolGateway.unwaitPacket(0x18, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
@@ -29,7 +28,7 @@ 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.EncryptedAccount
|
import com.rosetta.messenger.data.EncryptedAccount
|
||||||
import com.rosetta.messenger.session.AppSessionCoordinator
|
import com.rosetta.messenger.di.SessionCoordinator
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -39,6 +38,8 @@ fun SetPasswordScreen(
|
|||||||
seedPhrase: List<String>,
|
seedPhrase: List<String>,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
isImportMode: Boolean = false,
|
isImportMode: Boolean = false,
|
||||||
|
accountManager: AccountManager,
|
||||||
|
sessionCoordinator: SessionCoordinator,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onAccountCreated: (DecryptedAccount) -> Unit
|
onAccountCreated: (DecryptedAccount) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -47,8 +48,6 @@ fun SetPasswordScreen(
|
|||||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93)
|
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93)
|
||||||
val fieldBackground = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
|
val fieldBackground = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val accountManager = remember { AccountManager(context) }
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("") }
|
||||||
@@ -316,8 +315,7 @@ fun SetPasswordScreen(
|
|||||||
privateKeyHash = privateKeyHash,
|
privateKeyHash = privateKeyHash,
|
||||||
name = truncatedKey
|
name = truncatedKey
|
||||||
)
|
)
|
||||||
AppSessionCoordinator.bootstrapAuthenticatedSession(
|
sessionCoordinator.bootstrapAuthenticatedSession(
|
||||||
accountManager = accountManager,
|
|
||||||
account = decryptedAccount,
|
account = decryptedAccount,
|
||||||
reason = "set_password"
|
reason = "set_password"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,10 +27,9 @@ import androidx.compose.ui.unit.sp
|
|||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.rosetta.messenger.crypto.CryptoManager
|
import com.rosetta.messenger.crypto.CryptoManager
|
||||||
import com.rosetta.messenger.data.AccountManager
|
|
||||||
import com.rosetta.messenger.data.DecryptedAccount
|
import com.rosetta.messenger.data.DecryptedAccount
|
||||||
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.network.PacketUserInfo
|
import com.rosetta.messenger.network.PacketUserInfo
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
|
||||||
import com.rosetta.messenger.ui.icons.TelegramIcons
|
import com.rosetta.messenger.ui.icons.TelegramIcons
|
||||||
import com.rosetta.messenger.ui.settings.ProfilePhotoPicker
|
import com.rosetta.messenger.ui.settings.ProfilePhotoPicker
|
||||||
import com.rosetta.messenger.utils.AvatarFileManager
|
import com.rosetta.messenger.utils.AvatarFileManager
|
||||||
@@ -75,6 +74,9 @@ fun SetProfileScreen(
|
|||||||
onSkip: () -> Unit
|
onSkip: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val protocolGateway = remember(uiDeps) { uiDeps.protocolGateway() }
|
||||||
|
val accountManager = remember(uiDeps) { uiDeps.accountManager() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
var name by remember { mutableStateOf("") }
|
var name by remember { mutableStateOf("") }
|
||||||
@@ -104,7 +106,7 @@ fun SetProfileScreen(
|
|||||||
isCheckingUsername = true
|
isCheckingUsername = true
|
||||||
delay(600) // debounce
|
delay(600) // debounce
|
||||||
try {
|
try {
|
||||||
val results = ProtocolManager.searchUsers(trimmed, 3000)
|
val results = protocolGateway.searchUsers(trimmed, 3000)
|
||||||
val taken = results.any { it.username.equals(trimmed, ignoreCase = true) }
|
val taken = results.any { it.username.equals(trimmed, ignoreCase = true) }
|
||||||
usernameAvailable = !taken
|
usernameAvailable = !taken
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
@@ -402,14 +404,13 @@ fun SetProfileScreen(
|
|||||||
try {
|
try {
|
||||||
// Wait for server connection (up to 8s)
|
// Wait for server connection (up to 8s)
|
||||||
val connected = withTimeoutOrNull(8000) {
|
val connected = withTimeoutOrNull(8000) {
|
||||||
while (!ProtocolManager.isAuthenticated()) {
|
while (!protocolGateway.isAuthenticated()) {
|
||||||
delay(300)
|
delay(300)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
} ?: false
|
} ?: false
|
||||||
|
|
||||||
// Save name and username locally first
|
// Save name and username locally first
|
||||||
val accountManager = AccountManager(context)
|
|
||||||
if (name.trim().isNotEmpty()) {
|
if (name.trim().isNotEmpty()) {
|
||||||
accountManager.updateAccountName(account.publicKey, name.trim())
|
accountManager.updateAccountName(account.publicKey, name.trim())
|
||||||
}
|
}
|
||||||
@@ -417,7 +418,7 @@ fun SetProfileScreen(
|
|||||||
accountManager.updateAccountUsername(account.publicKey, username.trim())
|
accountManager.updateAccountUsername(account.publicKey, username.trim())
|
||||||
}
|
}
|
||||||
// Trigger UI refresh in MainActivity
|
// Trigger UI refresh in MainActivity
|
||||||
ProtocolManager.notifyOwnProfileUpdated()
|
protocolGateway.notifyOwnProfileUpdated()
|
||||||
|
|
||||||
// Send name and username to server
|
// Send name and username to server
|
||||||
if (connected && (name.trim().isNotEmpty() || username.trim().isNotEmpty())) {
|
if (connected && (name.trim().isNotEmpty() || username.trim().isNotEmpty())) {
|
||||||
@@ -425,16 +426,16 @@ fun SetProfileScreen(
|
|||||||
packet.title = name.trim()
|
packet.title = name.trim()
|
||||||
packet.username = username.trim()
|
packet.username = username.trim()
|
||||||
packet.privateKey = account.privateKeyHash
|
packet.privateKey = account.privateKeyHash
|
||||||
ProtocolManager.send(packet)
|
protocolGateway.send(packet)
|
||||||
delay(1500)
|
delay(1500)
|
||||||
|
|
||||||
// Повторяем для надёжности
|
// Повторяем для надёжности
|
||||||
if (ProtocolManager.isAuthenticated()) {
|
if (protocolGateway.isAuthenticated()) {
|
||||||
val packet2 = PacketUserInfo()
|
val packet2 = PacketUserInfo()
|
||||||
packet2.title = name.trim()
|
packet2.title = name.trim()
|
||||||
packet2.username = username.trim()
|
packet2.username = username.trim()
|
||||||
packet2.privateKey = account.privateKeyHash
|
packet2.privateKey = account.privateKeyHash
|
||||||
ProtocolManager.send(packet2)
|
protocolGateway.send(packet2)
|
||||||
delay(500)
|
delay(500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ import com.rosetta.messenger.data.DecryptedAccount
|
|||||||
import com.rosetta.messenger.data.EncryptedAccount
|
import com.rosetta.messenger.data.EncryptedAccount
|
||||||
import com.rosetta.messenger.data.resolveAccountDisplayName
|
import com.rosetta.messenger.data.resolveAccountDisplayName
|
||||||
import com.rosetta.messenger.database.RosettaDatabase
|
import com.rosetta.messenger.database.RosettaDatabase
|
||||||
|
import com.rosetta.messenger.di.SessionCoordinator
|
||||||
import com.rosetta.messenger.repository.AvatarRepository
|
import com.rosetta.messenger.repository.AvatarRepository
|
||||||
import com.rosetta.messenger.session.AppSessionCoordinator
|
|
||||||
import com.rosetta.messenger.ui.components.AvatarImage
|
import com.rosetta.messenger.ui.components.AvatarImage
|
||||||
import com.rosetta.messenger.ui.chats.getAvatarColor
|
import com.rosetta.messenger.ui.chats.getAvatarColor
|
||||||
import com.rosetta.messenger.ui.chats.getAvatarText
|
import com.rosetta.messenger.ui.chats.getAvatarText
|
||||||
@@ -69,6 +69,7 @@ private suspend fun performUnlock(
|
|||||||
selectedAccount: AccountItem?,
|
selectedAccount: AccountItem?,
|
||||||
password: String,
|
password: String,
|
||||||
accountManager: AccountManager,
|
accountManager: AccountManager,
|
||||||
|
sessionCoordinator: SessionCoordinator,
|
||||||
onUnlocking: (Boolean) -> Unit,
|
onUnlocking: (Boolean) -> Unit,
|
||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
onSuccess: (DecryptedAccount) -> Unit
|
onSuccess: (DecryptedAccount) -> Unit
|
||||||
@@ -117,8 +118,7 @@ val decryptedPrivateKey = CryptoManager.decryptWithPassword(
|
|||||||
name = selectedAccount.name
|
name = selectedAccount.name
|
||||||
)
|
)
|
||||||
|
|
||||||
AppSessionCoordinator.bootstrapAuthenticatedSession(
|
sessionCoordinator.bootstrapAuthenticatedSession(
|
||||||
accountManager = accountManager,
|
|
||||||
account = decryptedAccount,
|
account = decryptedAccount,
|
||||||
reason = "unlock"
|
reason = "unlock"
|
||||||
)
|
)
|
||||||
@@ -134,6 +134,8 @@ val decryptedPrivateKey = CryptoManager.decryptWithPassword(
|
|||||||
fun UnlockScreen(
|
fun UnlockScreen(
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
selectedAccountId: String? = null,
|
selectedAccountId: String? = null,
|
||||||
|
accountManager: AccountManager,
|
||||||
|
sessionCoordinator: SessionCoordinator,
|
||||||
onUnlocked: (DecryptedAccount) -> Unit,
|
onUnlocked: (DecryptedAccount) -> Unit,
|
||||||
onSwitchAccount: () -> Unit = {},
|
onSwitchAccount: () -> Unit = {},
|
||||||
onRecover: () -> Unit = {}
|
onRecover: () -> Unit = {}
|
||||||
@@ -163,7 +165,6 @@ fun UnlockScreen(
|
|||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val activity = context as? FragmentActivity
|
val activity = context as? FragmentActivity
|
||||||
val accountManager = remember { AccountManager(context) }
|
|
||||||
val biometricManager = remember { BiometricAuthManager(context) }
|
val biometricManager = remember { BiometricAuthManager(context) }
|
||||||
val biometricPrefs = remember { BiometricPreferences(context) }
|
val biometricPrefs = remember { BiometricPreferences(context) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -262,6 +263,7 @@ fun UnlockScreen(
|
|||||||
selectedAccount = selectedAccount,
|
selectedAccount = selectedAccount,
|
||||||
password = decryptedPassword,
|
password = decryptedPassword,
|
||||||
accountManager = accountManager,
|
accountManager = accountManager,
|
||||||
|
sessionCoordinator = sessionCoordinator,
|
||||||
onUnlocking = { isUnlocking = it },
|
onUnlocking = { isUnlocking = it },
|
||||||
onError = { error = it },
|
onError = { error = it },
|
||||||
onSuccess = { decryptedAccount ->
|
onSuccess = { decryptedAccount ->
|
||||||
@@ -607,6 +609,7 @@ fun UnlockScreen(
|
|||||||
selectedAccount = selectedAccount,
|
selectedAccount = selectedAccount,
|
||||||
password = password,
|
password = password,
|
||||||
accountManager = accountManager,
|
accountManager = accountManager,
|
||||||
|
sessionCoordinator = sessionCoordinator,
|
||||||
onUnlocking = { isUnlocking = it },
|
onUnlocking = { isUnlocking = it },
|
||||||
onError = { error = it },
|
onError = { error = it },
|
||||||
onSuccess = { decryptedAccount ->
|
onSuccess = { decryptedAccount ->
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ import com.airbnb.lottie.compose.LottieConstants
|
|||||||
import com.airbnb.lottie.compose.animateLottieCompositionAsState
|
import com.airbnb.lottie.compose.animateLottieCompositionAsState
|
||||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||||
import com.rosetta.messenger.R
|
import com.rosetta.messenger.R
|
||||||
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.data.ForwardManager
|
import com.rosetta.messenger.data.ForwardManager
|
||||||
import com.rosetta.messenger.data.GroupRepository
|
import com.rosetta.messenger.data.GroupRepository
|
||||||
import com.rosetta.messenger.data.MessageRepository
|
import com.rosetta.messenger.data.MessageRepository
|
||||||
@@ -100,7 +101,6 @@ import com.rosetta.messenger.database.RosettaDatabase
|
|||||||
import com.rosetta.messenger.network.AttachmentType
|
import com.rosetta.messenger.network.AttachmentType
|
||||||
import com.rosetta.messenger.network.CallManager
|
import com.rosetta.messenger.network.CallManager
|
||||||
import com.rosetta.messenger.network.CallPhase
|
import com.rosetta.messenger.network.CallPhase
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
|
||||||
import com.rosetta.messenger.network.SearchUser
|
import com.rosetta.messenger.network.SearchUser
|
||||||
import com.rosetta.messenger.ui.chats.calls.CallTopBanner
|
import com.rosetta.messenger.ui.chats.calls.CallTopBanner
|
||||||
import com.rosetta.messenger.repository.AvatarRepository
|
import com.rosetta.messenger.repository.AvatarRepository
|
||||||
@@ -342,6 +342,10 @@ fun ChatDetailScreen(
|
|||||||
) {
|
) {
|
||||||
val viewModel: ChatViewModel = viewModel(key = "chat_${user.publicKey}")
|
val viewModel: ChatViewModel = viewModel(key = "chat_${user.publicKey}")
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val protocolGateway = remember(uiDeps) { uiDeps.protocolGateway() }
|
||||||
|
val preferencesManager = remember(uiDeps) { uiDeps.preferencesManager() }
|
||||||
|
val messageRepository = remember(uiDeps) { uiDeps.messageRepository() }
|
||||||
val hasNativeNavigationBar = remember(context) {
|
val hasNativeNavigationBar = remember(context) {
|
||||||
com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)
|
com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)
|
||||||
}
|
}
|
||||||
@@ -354,7 +358,6 @@ fun ChatDetailScreen(
|
|||||||
val hapticFeedback = LocalHapticFeedback.current
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
|
|
||||||
// 🔇 Mute state — read from PreferencesManager
|
// 🔇 Mute state — read from PreferencesManager
|
||||||
val preferencesManager = remember { com.rosetta.messenger.data.PreferencesManager(context) }
|
|
||||||
val isChatMuted = remember { mutableStateOf(false) }
|
val isChatMuted = remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(currentUserPublicKey, user.publicKey) {
|
LaunchedEffect(currentUserPublicKey, user.publicKey) {
|
||||||
if (currentUserPublicKey.isNotBlank()) {
|
if (currentUserPublicKey.isNotBlank()) {
|
||||||
@@ -522,7 +525,7 @@ fun ChatDetailScreen(
|
|||||||
if (normalizedPublicKey.isBlank()) return@LaunchedEffect
|
if (normalizedPublicKey.isBlank()) return@LaunchedEffect
|
||||||
|
|
||||||
val cachedVerified =
|
val cachedVerified =
|
||||||
ProtocolManager.getCachedUserInfo(normalizedPublicKey)?.verified ?: 0
|
protocolGateway.getCachedUserInfo(normalizedPublicKey)?.verified ?: 0
|
||||||
if (cachedVerified > chatHeaderVerified) {
|
if (cachedVerified > chatHeaderVerified) {
|
||||||
chatHeaderVerified = cachedVerified
|
chatHeaderVerified = cachedVerified
|
||||||
}
|
}
|
||||||
@@ -733,7 +736,7 @@ fun ChatDetailScreen(
|
|||||||
// 📨 Forward: список диалогов для выбора (загружаем из базы)
|
// 📨 Forward: список диалогов для выбора (загружаем из базы)
|
||||||
val chatsListViewModel: ChatsListViewModel = viewModel()
|
val chatsListViewModel: ChatsListViewModel = viewModel()
|
||||||
val dialogsList by chatsListViewModel.dialogs.collectAsState()
|
val dialogsList by chatsListViewModel.dialogs.collectAsState()
|
||||||
val groupRepository = remember { GroupRepository.getInstance(context) }
|
val groupRepository = remember(uiDeps) { uiDeps.groupRepository() }
|
||||||
val groupMembersCacheKey =
|
val groupMembersCacheKey =
|
||||||
remember(user.publicKey, currentUserPublicKey) {
|
remember(user.publicKey, currentUserPublicKey) {
|
||||||
"${currentUserPublicKey.trim()}::${user.publicKey.trim()}"
|
"${currentUserPublicKey.trim()}::${user.publicKey.trim()}"
|
||||||
@@ -4251,10 +4254,7 @@ fun ChatDetailScreen(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
if (isLeaveGroupDialog) {
|
if (isLeaveGroupDialog) {
|
||||||
GroupRepository
|
groupRepository
|
||||||
.getInstance(
|
|
||||||
context
|
|
||||||
)
|
|
||||||
.leaveGroup(
|
.leaveGroup(
|
||||||
currentUserPublicKey,
|
currentUserPublicKey,
|
||||||
user.publicKey
|
user.publicKey
|
||||||
@@ -4271,11 +4271,7 @@ fun ChatDetailScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🗑️ Очищаем ВСЕ кэши сообщений
|
// 🗑️ Очищаем ВСЕ кэши сообщений
|
||||||
com.rosetta.messenger.data
|
messageRepository
|
||||||
.MessageRepository
|
|
||||||
.getInstance(
|
|
||||||
context
|
|
||||||
)
|
|
||||||
.clearDialogCache(
|
.clearDialogCache(
|
||||||
user.publicKey
|
user.publicKey
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.rosetta.messenger.crypto.CryptoManager
|
import com.rosetta.messenger.crypto.CryptoManager
|
||||||
import com.rosetta.messenger.crypto.MessageCrypto
|
import com.rosetta.messenger.crypto.MessageCrypto
|
||||||
import com.rosetta.messenger.data.ForwardManager
|
import com.rosetta.messenger.data.ForwardManager
|
||||||
|
import com.rosetta.messenger.di.ProtocolGateway
|
||||||
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.database.MessageEntity
|
import com.rosetta.messenger.database.MessageEntity
|
||||||
import com.rosetta.messenger.database.RosettaDatabase
|
import com.rosetta.messenger.database.RosettaDatabase
|
||||||
import com.rosetta.messenger.network.*
|
import com.rosetta.messenger.network.*
|
||||||
@@ -121,16 +123,17 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
private val searchIndexDao = database.messageSearchIndexDao()
|
private val searchIndexDao = database.messageSearchIndexDao()
|
||||||
private val groupDao = database.groupDao()
|
private val groupDao = database.groupDao()
|
||||||
private val pinnedMessageDao = database.pinnedMessageDao()
|
private val pinnedMessageDao = database.pinnedMessageDao()
|
||||||
|
private val uiDeps = UiDependencyAccess.get(application)
|
||||||
|
private val protocolGateway: ProtocolGateway = uiDeps.protocolGateway()
|
||||||
|
|
||||||
// MessageRepository для подписки на события новых сообщений
|
// MessageRepository для подписки на события новых сообщений
|
||||||
private val messageRepository =
|
private val messageRepository = uiDeps.messageRepository()
|
||||||
com.rosetta.messenger.data.MessageRepository.getInstance(application)
|
|
||||||
private val sendTextMessageUseCase =
|
private val sendTextMessageUseCase =
|
||||||
SendTextMessageUseCase(sendWithRetry = { packet -> ProtocolManager.sendMessageWithRetry(packet) })
|
SendTextMessageUseCase(sendWithRetry = { packet -> protocolGateway.sendMessageWithRetry(packet) })
|
||||||
private val sendMediaMessageUseCase =
|
private val sendMediaMessageUseCase =
|
||||||
SendMediaMessageUseCase(sendWithRetry = { packet -> ProtocolManager.sendMessageWithRetry(packet) })
|
SendMediaMessageUseCase(sendWithRetry = { packet -> protocolGateway.sendMessageWithRetry(packet) })
|
||||||
private val sendForwardUseCase =
|
private val sendForwardUseCase =
|
||||||
SendForwardUseCase(sendWithRetry = { packet -> ProtocolManager.sendMessageWithRetry(packet) })
|
SendForwardUseCase(sendWithRetry = { packet -> protocolGateway.sendMessageWithRetry(packet) })
|
||||||
|
|
||||||
// 🔥 Кэш расшифрованных сообщений (messageId -> plainText)
|
// 🔥 Кэш расшифрованных сообщений (messageId -> plainText)
|
||||||
private val decryptionCache = ConcurrentHashMap<String, String>()
|
private val decryptionCache = ConcurrentHashMap<String, String>()
|
||||||
@@ -722,7 +725,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
typingSnapshotJob?.cancel()
|
typingSnapshotJob?.cancel()
|
||||||
typingSnapshotJob =
|
typingSnapshotJob =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
ProtocolManager.typingUsersByDialogSnapshot.collect { snapshot ->
|
protocolGateway.typingUsersByDialogSnapshot.collect { snapshot ->
|
||||||
val currentDialog = opponentKey?.trim().orEmpty()
|
val currentDialog = opponentKey?.trim().orEmpty()
|
||||||
val currentAccount = myPublicKey?.trim().orEmpty()
|
val currentAccount = myPublicKey?.trim().orEmpty()
|
||||||
|
|
||||||
@@ -923,12 +926,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun logPhotoPipeline(messageId: String, message: String) {
|
private fun logPhotoPipeline(messageId: String, message: String) {
|
||||||
ProtocolManager.addLog("📸 IMG ${shortPhotoId(messageId)} | $message")
|
protocolGateway.addLog("📸 IMG ${shortPhotoId(messageId)} | $message")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logPhotoPipelineError(messageId: String, stage: String, throwable: Throwable) {
|
private fun logPhotoPipelineError(messageId: String, stage: String, throwable: Throwable) {
|
||||||
val reason = throwable.message ?: "unknown"
|
val reason = throwable.message ?: "unknown"
|
||||||
ProtocolManager.addLog(
|
protocolGateway.addLog(
|
||||||
"❌ IMG ${shortPhotoId(messageId)} | $stage failed: ${throwable.javaClass.simpleName}: $reason"
|
"❌ IMG ${shortPhotoId(messageId)} | $stage failed: ${throwable.javaClass.simpleName}: $reason"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1046,7 +1049,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
if (!isGroupDialogKey(normalizedPublicKey)) {
|
if (!isGroupDialogKey(normalizedPublicKey)) {
|
||||||
groupKeyCache.remove(normalizedPublicKey)
|
groupKeyCache.remove(normalizedPublicKey)
|
||||||
}
|
}
|
||||||
ProtocolManager.addLog("🔧 SEND_CONTEXT opponent=${shortSendKey(normalizedPublicKey)}")
|
protocolGateway.addLog("🔧 SEND_CONTEXT opponent=${shortSendKey(normalizedPublicKey)}")
|
||||||
triggerPendingTextSendIfReady("send_context_bound")
|
triggerPendingTextSendIfReady("send_context_bound")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1774,7 +1777,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return nameFromMessages
|
return nameFromMessages
|
||||||
}
|
}
|
||||||
|
|
||||||
val cachedInfo = ProtocolManager.getCachedUserInfo(normalizedPublicKey)
|
val cachedInfo = protocolGateway.getCachedUserInfo(normalizedPublicKey)
|
||||||
val protocolName =
|
val protocolName =
|
||||||
cachedInfo?.title?.trim().orEmpty().ifBlank { cachedInfo?.username?.trim().orEmpty() }
|
cachedInfo?.title?.trim().orEmpty().ifBlank { cachedInfo?.username?.trim().orEmpty() }
|
||||||
if (isUsableSenderName(protocolName, normalizedPublicKey)) {
|
if (isUsableSenderName(protocolName, normalizedPublicKey)) {
|
||||||
@@ -1808,7 +1811,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val cached = ProtocolManager.getCachedUserInfo(normalizedPublicKey)
|
val cached = protocolGateway.getCachedUserInfo(normalizedPublicKey)
|
||||||
val protocolName = cached?.title?.trim().orEmpty()
|
val protocolName = cached?.title?.trim().orEmpty()
|
||||||
.ifBlank { cached?.username?.trim().orEmpty() }
|
.ifBlank { cached?.username?.trim().orEmpty() }
|
||||||
if (isUsableSenderName(protocolName, normalizedPublicKey)) {
|
if (isUsableSenderName(protocolName, normalizedPublicKey)) {
|
||||||
@@ -1842,7 +1845,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val resolved = ProtocolManager.resolveUserInfo(normalizedPublicKey, timeoutMs = 5000L)
|
val resolved = protocolGateway.resolveUserInfo(normalizedPublicKey, timeoutMs = 5000L)
|
||||||
val name = resolved?.title?.trim().orEmpty()
|
val name = resolved?.title?.trim().orEmpty()
|
||||||
.ifBlank { resolved?.username?.trim().orEmpty() }
|
.ifBlank { resolved?.username?.trim().orEmpty() }
|
||||||
if (!isUsableSenderName(name, normalizedPublicKey)) {
|
if (!isUsableSenderName(name, normalizedPublicKey)) {
|
||||||
@@ -2568,12 +2571,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
|
|
||||||
// 2. Try ProtocolManager cache (previously resolved)
|
// 2. Try ProtocolManager cache (previously resolved)
|
||||||
val cachedName = dbName ?: ProtocolManager.getCachedUserName(fwdPublicKey)
|
val cachedName = dbName ?: protocolGateway.getCachedUserName(fwdPublicKey)
|
||||||
|
|
||||||
// 3. Server resolve via PacketSearch (like desktop useUserInformation)
|
// 3. Server resolve via PacketSearch (like desktop useUserInformation)
|
||||||
val serverName = if (cachedName == null) {
|
val serverName = if (cachedName == null) {
|
||||||
try {
|
try {
|
||||||
ProtocolManager.resolveUserName(fwdPublicKey, 3000)
|
protocolGateway.resolveUserName(fwdPublicKey, 3000)
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
@@ -2750,11 +2753,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
fwdDialog?.opponentTitle?.ifEmpty { fwdDialog.opponentUsername }?.ifEmpty { null }
|
fwdDialog?.opponentTitle?.ifEmpty { fwdDialog.opponentUsername }?.ifEmpty { null }
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
// 2. Try ProtocolManager cache
|
// 2. Try ProtocolManager cache
|
||||||
val cachedName = dbName ?: ProtocolManager.getCachedUserName(replyPublicKey)
|
val cachedName = dbName ?: protocolGateway.getCachedUserName(replyPublicKey)
|
||||||
// 3. Server resolve via PacketSearch (like desktop useUserInformation)
|
// 3. Server resolve via PacketSearch (like desktop useUserInformation)
|
||||||
val serverName = if (cachedName == null) {
|
val serverName = if (cachedName == null) {
|
||||||
try {
|
try {
|
||||||
ProtocolManager.resolveUserName(replyPublicKey, 3000)
|
protocolGateway.resolveUserName(replyPublicKey, 3000)
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
} else null
|
} else null
|
||||||
cachedName ?: serverName ?: "User"
|
cachedName ?: serverName ?: "User"
|
||||||
@@ -3388,14 +3391,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
/** 🛑 Отменить исходящую отправку фото во время загрузки */
|
/** 🛑 Отменить исходящую отправку фото во время загрузки */
|
||||||
fun cancelOutgoingImageUpload(messageId: String, attachmentId: String) {
|
fun cancelOutgoingImageUpload(messageId: String, attachmentId: String) {
|
||||||
ProtocolManager.addLog(
|
protocolGateway.addLog(
|
||||||
"🛑 IMG cancel requested: msg=${messageId.take(8)}, att=${attachmentId.take(12)}"
|
"🛑 IMG cancel requested: msg=${messageId.take(8)}, att=${attachmentId.take(12)}"
|
||||||
)
|
)
|
||||||
outgoingImageUploadJobs.remove(messageId)?.cancel(
|
outgoingImageUploadJobs.remove(messageId)?.cancel(
|
||||||
CancellationException("User cancelled image upload")
|
CancellationException("User cancelled image upload")
|
||||||
)
|
)
|
||||||
TransportManager.cancelUpload(attachmentId)
|
TransportManager.cancelUpload(attachmentId)
|
||||||
ProtocolManager.resolveOutgoingRetry(messageId)
|
protocolGateway.resolveOutgoingRetry(messageId)
|
||||||
deleteMessage(messageId)
|
deleteMessage(messageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3470,7 +3473,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
|
|
||||||
// 2. Try ProtocolManager cache
|
// 2. Try ProtocolManager cache
|
||||||
val cached = ProtocolManager.getCachedUserInfo(publicKey)
|
val cached = protocolGateway.getCachedUserInfo(publicKey)
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
return SearchUser(
|
return SearchUser(
|
||||||
title = cached.title,
|
title = cached.title,
|
||||||
@@ -3483,7 +3486,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
// 3. Server resolve
|
// 3. Server resolve
|
||||||
try {
|
try {
|
||||||
val resolved = ProtocolManager.resolveUserInfo(publicKey, 3000)
|
val resolved = protocolGateway.resolveUserInfo(publicKey, 3000)
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
return SearchUser(
|
return SearchUser(
|
||||||
title = resolved.title,
|
title = resolved.title,
|
||||||
@@ -3532,10 +3535,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2) In-memory protocol cache.
|
// 2) In-memory protocol cache.
|
||||||
ProtocolManager.getCachedUserByUsername(normalized)?.let { return it }
|
protocolGateway.getCachedUserByUsername(normalized)?.let { return it }
|
||||||
|
|
||||||
// 3) Server search fallback.
|
// 3) Server search fallback.
|
||||||
val results = ProtocolManager.searchUsers(normalized, timeoutMs)
|
val results = protocolGateway.searchUsers(normalized, timeoutMs)
|
||||||
if (results.isEmpty()) return null
|
if (results.isEmpty()) return null
|
||||||
|
|
||||||
return results.firstOrNull {
|
return results.firstOrNull {
|
||||||
@@ -3604,7 +3607,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
sender: String?,
|
sender: String?,
|
||||||
hasPrivateKey: Boolean
|
hasPrivateKey: Boolean
|
||||||
) {
|
) {
|
||||||
ProtocolManager.addLog(
|
protocolGateway.addLog(
|
||||||
"⚠️ SEND_BLOCKED reason=$reason textLen=$textLength hasReply=$hasReply recipient=${shortSendKey(recipient)} sender=${shortSendKey(sender)} hasPriv=$hasPrivateKey isSending=$isSending"
|
"⚠️ SEND_BLOCKED reason=$reason textLen=$textLength hasReply=$hasReply recipient=${shortSendKey(recipient)} sender=${shortSendKey(sender)} hasPriv=$hasPrivateKey isSending=$isSending"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3618,7 +3621,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
if (repositoryPublicKey.isNotEmpty() && repositoryPrivateKey.isNotEmpty()) {
|
if (repositoryPublicKey.isNotEmpty() && repositoryPrivateKey.isNotEmpty()) {
|
||||||
setUserKeys(repositoryPublicKey, repositoryPrivateKey)
|
setUserKeys(repositoryPublicKey, repositoryPrivateKey)
|
||||||
ProtocolManager.addLog(
|
protocolGateway.addLog(
|
||||||
"🔄 SEND_RECOVERY restored_keys pk=${shortSendKey(repositoryPublicKey)}"
|
"🔄 SEND_RECOVERY restored_keys pk=${shortSendKey(repositoryPublicKey)}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3633,7 +3636,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
if (pendingSendRecoveryJob?.isActive == true) return
|
if (pendingSendRecoveryJob?.isActive == true) return
|
||||||
|
|
||||||
ProtocolManager.addLog("⏳ SEND_RECOVERY queued reason=$reason")
|
protocolGateway.addLog("⏳ SEND_RECOVERY queued reason=$reason")
|
||||||
pendingSendRecoveryJob =
|
pendingSendRecoveryJob =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repeat(10) { attempt ->
|
repeat(10) { attempt ->
|
||||||
@@ -3644,7 +3647,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pendingTextSendRequested) {
|
if (pendingTextSendRequested) {
|
||||||
ProtocolManager.addLog("⚠️ SEND_RECOVERY timeout reason=$pendingTextSendReason")
|
protocolGateway.addLog("⚠️ SEND_RECOVERY timeout reason=$pendingTextSendReason")
|
||||||
}
|
}
|
||||||
pendingTextSendRequested = false
|
pendingTextSendRequested = false
|
||||||
pendingTextSendReason = ""
|
pendingTextSendReason = ""
|
||||||
@@ -3668,7 +3671,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val keysReady = !myPublicKey.isNullOrBlank() && !myPrivateKey.isNullOrBlank()
|
val keysReady = !myPublicKey.isNullOrBlank() && !myPrivateKey.isNullOrBlank()
|
||||||
if (!recipientReady || !keysReady || isSending) return
|
if (!recipientReady || !keysReady || isSending) return
|
||||||
|
|
||||||
ProtocolManager.addLog("🚀 SEND_RECOVERY flush trigger=$trigger")
|
protocolGateway.addLog("🚀 SEND_RECOVERY flush trigger=$trigger")
|
||||||
pendingTextSendRequested = false
|
pendingTextSendRequested = false
|
||||||
pendingTextSendReason = ""
|
pendingTextSendReason = ""
|
||||||
pendingSendRecoveryJob?.cancel()
|
pendingSendRecoveryJob?.cancel()
|
||||||
@@ -4385,7 +4388,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val context = getApplication<Application>()
|
val context = getApplication<Application>()
|
||||||
|
|
||||||
if (recipient == null || sender == null || privateKey == null) {
|
if (recipient == null || sender == null || privateKey == null) {
|
||||||
ProtocolManager.addLog(
|
protocolGateway.addLog(
|
||||||
"❌ IMG send aborted: missing keys (recipient=${recipient != null}, sender=${sender != null}, private=${privateKey != null})"
|
"❌ IMG send aborted: missing keys (recipient=${recipient != null}, sender=${sender != null}, private=${privateKey != null})"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -4986,13 +4989,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val groupDebugId = UUID.randomUUID().toString().replace("-", "").take(8)
|
val groupDebugId = UUID.randomUUID().toString().replace("-", "").take(8)
|
||||||
|
|
||||||
if (recipient == null || sender == null || privateKey == null) {
|
if (recipient == null || sender == null || privateKey == null) {
|
||||||
ProtocolManager.addLog(
|
protocolGateway.addLog(
|
||||||
"❌ IMG-GROUP $groupDebugId | aborted: missing keys (recipient=${recipient != null}, sender=${sender != null}, private=${privateKey != null})"
|
"❌ IMG-GROUP $groupDebugId | aborted: missing keys (recipient=${recipient != null}, sender=${sender != null}, private=${privateKey != null})"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isSending) {
|
if (isSending) {
|
||||||
ProtocolManager.addLog("⚠️ IMG-GROUP $groupDebugId | skipped: another send in progress")
|
protocolGateway.addLog("⚠️ IMG-GROUP $groupDebugId | skipped: another send in progress")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5028,7 +5031,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
)
|
)
|
||||||
_inputText.value = ""
|
_inputText.value = ""
|
||||||
|
|
||||||
ProtocolManager.addLog(
|
protocolGateway.addLog(
|
||||||
"📸 IMG-GROUP $groupDebugId | prepare start: count=${imageUris.size}, captionLen=${caption.trim().length}"
|
"📸 IMG-GROUP $groupDebugId | prepare start: count=${imageUris.size}, captionLen=${caption.trim().length}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -5073,7 +5076,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
opponentPublicKey = recipient
|
opponentPublicKey = recipient
|
||||||
)
|
)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
ProtocolManager.addLog("⚠️ IMG-GROUP $groupDebugId | optimistic DB save skipped (non-fatal)")
|
protocolGateway.addLog("⚠️ IMG-GROUP $groupDebugId | optimistic DB save skipped (non-fatal)")
|
||||||
}
|
}
|
||||||
|
|
||||||
val preparedImages =
|
val preparedImages =
|
||||||
@@ -5089,7 +5092,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
uri
|
uri
|
||||||
)
|
)
|
||||||
?: run {
|
?: run {
|
||||||
ProtocolManager.addLog(
|
protocolGateway.addLog(
|
||||||
"❌ IMG-GROUP $groupDebugId | item#$index base64 conversion failed"
|
"❌ IMG-GROUP $groupDebugId | item#$index base64 conversion failed"
|
||||||
)
|
)
|
||||||
throw IllegalStateException(
|
throw IllegalStateException(
|
||||||
@@ -5101,7 +5104,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
context,
|
context,
|
||||||
uri
|
uri
|
||||||
)
|
)
|
||||||
ProtocolManager.addLog(
|
protocolGateway.addLog(
|
||||||
"📸 IMG-GROUP $groupDebugId | item#$index prepared: ${width}x$height, base64Len=${imageBase64.length}, blurhashLen=${blurhash.length}"
|
"📸 IMG-GROUP $groupDebugId | item#$index prepared: ${width}x$height, base64Len=${imageBase64.length}, blurhashLen=${blurhash.length}"
|
||||||
)
|
)
|
||||||
index to
|
index to
|
||||||
@@ -5114,7 +5117,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (preparedImages.isEmpty()) {
|
if (preparedImages.isEmpty()) {
|
||||||
ProtocolManager.addLog(
|
protocolGateway.addLog(
|
||||||
"❌ IMG-GROUP $groupDebugId | no prepared images, send canceled"
|
"❌ IMG-GROUP $groupDebugId | no prepared images, send canceled"
|
||||||
)
|
)
|
||||||
updateMessageStatusInDb(messageId, DeliveryStatus.ERROR.value)
|
updateMessageStatusInDb(messageId, DeliveryStatus.ERROR.value)
|
||||||
@@ -5122,7 +5125,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
isSending = false
|
isSending = false
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
ProtocolManager.addLog(
|
protocolGateway.addLog(
|
||||||
"📸 IMG-GROUP $groupDebugId | prepare done: ready=${preparedImages.size}"
|
"📸 IMG-GROUP $groupDebugId | prepare done: ready=${preparedImages.size}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -6653,7 +6656,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
dialogDao.markIHaveSent(account, opponent)
|
dialogDao.markIHaveSent(account, opponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
ProtocolManager.addLog(
|
protocolGateway.addLog(
|
||||||
"🛠️ DIALOG_FALLBACK upserted opponent=${opponent.take(12)} ts=$fallbackTimestamp hasContent=1"
|
"🛠️ DIALOG_FALLBACK upserted opponent=${opponent.take(12)} ts=$fallbackTimestamp hasContent=1"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -6920,7 +6923,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
toPublicKey = opponent
|
toPublicKey = opponent
|
||||||
}
|
}
|
||||||
|
|
||||||
ProtocolManager.send(packet)
|
protocolGateway.send(packet)
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6964,7 +6967,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
toPublicKey = opponent // Кому отправляем уведомление (собеседник)
|
toPublicKey = opponent // Кому отправляем уведомление (собеседник)
|
||||||
}
|
}
|
||||||
|
|
||||||
ProtocolManager.send(packet)
|
protocolGateway.send(packet)
|
||||||
// ✅ Обновляем timestamp ПОСЛЕ успешной отправки
|
// ✅ Обновляем timestamp ПОСЛЕ успешной отправки
|
||||||
lastReadMessageTimestamp = incomingTs
|
lastReadMessageTimestamp = incomingTs
|
||||||
MessageLogger.logReadReceiptSent(opponent)
|
MessageLogger.logReadReceiptSent(opponent)
|
||||||
@@ -6973,7 +6976,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
// 🔄 Retry через 2с (desktop буферизует через WebSocket, мы ретраим вручную)
|
// 🔄 Retry через 2с (desktop буферизует через WebSocket, мы ретраим вручную)
|
||||||
try {
|
try {
|
||||||
kotlinx.coroutines.delay(2000)
|
kotlinx.coroutines.delay(2000)
|
||||||
ProtocolManager.send(
|
protocolGateway.send(
|
||||||
PacketRead().apply {
|
PacketRead().apply {
|
||||||
this.privateKey = CryptoManager.generatePrivateKeyHash(privateKey)
|
this.privateKey = CryptoManager.generatePrivateKeyHash(privateKey)
|
||||||
fromPublicKey = sender
|
fromPublicKey = sender
|
||||||
@@ -7058,7 +7061,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
addPublicKey(opponent)
|
addPublicKey(opponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
ProtocolManager.send(packet)
|
protocolGateway.send(packet)
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,11 +68,12 @@ import com.rosetta.messenger.data.MessageRepository
|
|||||||
import com.rosetta.messenger.data.RecentSearchesManager
|
import com.rosetta.messenger.data.RecentSearchesManager
|
||||||
import com.rosetta.messenger.data.isPlaceholderAccountName
|
import com.rosetta.messenger.data.isPlaceholderAccountName
|
||||||
import com.rosetta.messenger.data.resolveAccountDisplayName
|
import com.rosetta.messenger.data.resolveAccountDisplayName
|
||||||
|
import com.rosetta.messenger.di.ProtocolGateway
|
||||||
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.database.RosettaDatabase
|
import com.rosetta.messenger.database.RosettaDatabase
|
||||||
import com.rosetta.messenger.network.CallPhase
|
import com.rosetta.messenger.network.CallPhase
|
||||||
import com.rosetta.messenger.network.CallUiState
|
import com.rosetta.messenger.network.CallUiState
|
||||||
import com.rosetta.messenger.network.DeviceEntry
|
import com.rosetta.messenger.network.DeviceEntry
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
|
||||||
import com.rosetta.messenger.network.ProtocolState
|
import com.rosetta.messenger.network.ProtocolState
|
||||||
import com.rosetta.messenger.repository.AvatarRepository
|
import com.rosetta.messenger.repository.AvatarRepository
|
||||||
import com.rosetta.messenger.ui.chats.calls.CallsHistoryScreen
|
import com.rosetta.messenger.ui.chats.calls.CallsHistoryScreen
|
||||||
@@ -246,10 +247,10 @@ private fun shortPublicKey(value: String): String {
|
|||||||
return "${trimmed.take(6)}...${trimmed.takeLast(4)}"
|
return "${trimmed.take(6)}...${trimmed.takeLast(4)}"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveTypingDisplayName(publicKey: String): String {
|
private fun resolveTypingDisplayName(protocolGateway: ProtocolGateway, publicKey: String): String {
|
||||||
val normalized = publicKey.trim()
|
val normalized = publicKey.trim()
|
||||||
if (normalized.isBlank()) return ""
|
if (normalized.isBlank()) return ""
|
||||||
val cached = ProtocolManager.getCachedUserInfo(normalized)
|
val cached = protocolGateway.getCachedUserInfo(normalized)
|
||||||
val resolvedName =
|
val resolvedName =
|
||||||
cached?.title?.trim().orEmpty().ifBlank { cached?.username?.trim().orEmpty() }
|
cached?.title?.trim().orEmpty().ifBlank { cached?.username?.trim().orEmpty() }
|
||||||
return if (resolvedName.isNotBlank()) resolvedName else shortPublicKey(normalized)
|
return if (resolvedName.isNotBlank()) resolvedName else shortPublicKey(normalized)
|
||||||
@@ -324,6 +325,10 @@ fun ChatsListScreen(
|
|||||||
|
|
||||||
val view = androidx.compose.ui.platform.LocalView.current
|
val view = androidx.compose.ui.platform.LocalView.current
|
||||||
val context = androidx.compose.ui.platform.LocalContext.current
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val protocolGateway = remember(uiDeps) { uiDeps.protocolGateway() }
|
||||||
|
val accountManager = remember(uiDeps) { uiDeps.accountManager() }
|
||||||
|
val preferencesManager = remember(uiDeps) { uiDeps.preferencesManager() }
|
||||||
val focusManager = androidx.compose.ui.platform.LocalFocusManager.current
|
val focusManager = androidx.compose.ui.platform.LocalFocusManager.current
|
||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -456,10 +461,10 @@ fun ChatsListScreen(
|
|||||||
remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E8E) else Color(0xFF8E8E93) }
|
remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E8E) else Color(0xFF8E8E93) }
|
||||||
|
|
||||||
// Protocol connection state
|
// Protocol connection state
|
||||||
val protocolState by ProtocolManager.state.collectAsState()
|
val protocolState by protocolGateway.state.collectAsState()
|
||||||
val syncInProgress by ProtocolManager.syncInProgress.collectAsState()
|
val syncInProgress by protocolGateway.syncInProgress.collectAsState()
|
||||||
val ownProfileUpdated by ProtocolManager.ownProfileUpdated.collectAsState()
|
val ownProfileUpdated by protocolGateway.ownProfileUpdated.collectAsState()
|
||||||
val pendingDeviceVerification by ProtocolManager.pendingDeviceVerification.collectAsState()
|
val pendingDeviceVerification by protocolGateway.pendingDeviceVerification.collectAsState()
|
||||||
|
|
||||||
// 📥 Active FILE downloads tracking (account-scoped, excludes photo downloads)
|
// 📥 Active FILE downloads tracking (account-scoped, excludes photo downloads)
|
||||||
val currentAccountKey = remember(accountPublicKey) { accountPublicKey.trim() }
|
val currentAccountKey = remember(accountPublicKey) { accountPublicKey.trim() }
|
||||||
@@ -490,8 +495,8 @@ fun ChatsListScreen(
|
|||||||
val hasActiveDownloads = activeFileDownloads.isNotEmpty()
|
val hasActiveDownloads = activeFileDownloads.isNotEmpty()
|
||||||
|
|
||||||
// <20>🔥 Пользователи, которые сейчас печатают
|
// <20>🔥 Пользователи, которые сейчас печатают
|
||||||
val typingUsers by ProtocolManager.typingUsers.collectAsState()
|
val typingUsers by protocolGateway.typingUsers.collectAsState()
|
||||||
val typingUsersByDialogSnapshot by ProtocolManager.typingUsersByDialogSnapshot.collectAsState()
|
val typingUsersByDialogSnapshot by protocolGateway.typingUsersByDialogSnapshot.collectAsState()
|
||||||
val playingVoiceAttachmentId by VoicePlaybackCoordinator.playingAttachmentId.collectAsState()
|
val playingVoiceAttachmentId by VoicePlaybackCoordinator.playingAttachmentId.collectAsState()
|
||||||
val playingVoiceDialogKey by VoicePlaybackCoordinator.playingDialogKey.collectAsState()
|
val playingVoiceDialogKey by VoicePlaybackCoordinator.playingDialogKey.collectAsState()
|
||||||
val isVoicePlaybackRunning by VoicePlaybackCoordinator.isPlaying.collectAsState()
|
val isVoicePlaybackRunning by VoicePlaybackCoordinator.isPlaying.collectAsState()
|
||||||
@@ -622,7 +627,6 @@ fun ChatsListScreen(
|
|||||||
// 👥 Load all accounts for sidebar (current account always first)
|
// 👥 Load all accounts for sidebar (current account always first)
|
||||||
var allAccounts by remember { mutableStateOf<List<EncryptedAccount>>(emptyList()) }
|
var allAccounts by remember { mutableStateOf<List<EncryptedAccount>>(emptyList()) }
|
||||||
LaunchedEffect(accountPublicKey, ownProfileUpdated) {
|
LaunchedEffect(accountPublicKey, ownProfileUpdated) {
|
||||||
val accountManager = AccountManager(context)
|
|
||||||
var accounts = accountManager.getAllAccounts()
|
var accounts = accountManager.getAllAccounts()
|
||||||
val preferredPublicKey =
|
val preferredPublicKey =
|
||||||
accountPublicKey.trim().ifBlank {
|
accountPublicKey.trim().ifBlank {
|
||||||
@@ -630,7 +634,7 @@ fun ChatsListScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (preferredPublicKey.isNotBlank()) {
|
if (preferredPublicKey.isNotBlank()) {
|
||||||
val cachedOwn = ProtocolManager.getCachedUserInfo(preferredPublicKey)
|
val cachedOwn = protocolGateway.getCachedUserInfo(preferredPublicKey)
|
||||||
val cachedTitle = cachedOwn?.title?.trim().orEmpty()
|
val cachedTitle = cachedOwn?.title?.trim().orEmpty()
|
||||||
val cachedUsername = cachedOwn?.username?.trim().orEmpty()
|
val cachedUsername = cachedOwn?.username?.trim().orEmpty()
|
||||||
val existing =
|
val existing =
|
||||||
@@ -711,7 +715,6 @@ fun ChatsListScreen(
|
|||||||
val isSelectionMode = selectedChatKeys.isNotEmpty()
|
val isSelectionMode = selectedChatKeys.isNotEmpty()
|
||||||
val hapticFeedback = LocalHapticFeedback.current
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
var showSelectionMenu by remember { mutableStateOf(false) }
|
var showSelectionMenu by remember { mutableStateOf(false) }
|
||||||
val preferencesManager = remember { com.rosetta.messenger.data.PreferencesManager(context) }
|
|
||||||
val mutedChats by preferencesManager.mutedChatsForAccount(effectiveCurrentPublicKey)
|
val mutedChats by preferencesManager.mutedChatsForAccount(effectiveCurrentPublicKey)
|
||||||
.collectAsState(initial = emptySet())
|
.collectAsState(initial = emptySet())
|
||||||
|
|
||||||
@@ -2730,6 +2733,7 @@ fun ChatsListScreen(
|
|||||||
} else {
|
} else {
|
||||||
val baseName =
|
val baseName =
|
||||||
resolveTypingDisplayName(
|
resolveTypingDisplayName(
|
||||||
|
protocolGateway,
|
||||||
typingSenderPublicKey
|
typingSenderPublicKey
|
||||||
)
|
)
|
||||||
if (baseName.isBlank()) {
|
if (baseName.isBlank()) {
|
||||||
@@ -3237,12 +3241,12 @@ fun ChatsListScreen(
|
|||||||
if (request != null) {
|
if (request != null) {
|
||||||
when (request.second) {
|
when (request.second) {
|
||||||
DeviceResolveAction.ACCEPT -> {
|
DeviceResolveAction.ACCEPT -> {
|
||||||
ProtocolManager.acceptDevice(
|
protocolGateway.acceptDevice(
|
||||||
request.first.deviceId
|
request.first.deviceId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DeviceResolveAction.DECLINE -> {
|
DeviceResolveAction.DECLINE -> {
|
||||||
ProtocolManager.declineDevice(
|
protocolGateway.declineDevice(
|
||||||
request.first.deviceId
|
request.first.deviceId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import com.rosetta.messenger.crypto.CryptoManager
|
|||||||
import com.rosetta.messenger.data.DraftManager
|
import com.rosetta.messenger.data.DraftManager
|
||||||
import com.rosetta.messenger.data.GroupRepository
|
import com.rosetta.messenger.data.GroupRepository
|
||||||
import com.rosetta.messenger.data.MessageRepository
|
import com.rosetta.messenger.data.MessageRepository
|
||||||
|
import com.rosetta.messenger.di.ProtocolGateway
|
||||||
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.database.BlacklistEntity
|
import com.rosetta.messenger.database.BlacklistEntity
|
||||||
import com.rosetta.messenger.database.RosettaDatabase
|
import com.rosetta.messenger.database.RosettaDatabase
|
||||||
import com.rosetta.messenger.network.PacketOnlineSubscribe
|
import com.rosetta.messenger.network.PacketOnlineSubscribe
|
||||||
import com.rosetta.messenger.network.PacketSearch
|
import com.rosetta.messenger.network.PacketSearch
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
|
||||||
import com.rosetta.messenger.network.SearchUser
|
import com.rosetta.messenger.network.SearchUser
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -74,7 +75,10 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
|
|
||||||
private val database = RosettaDatabase.getDatabase(application)
|
private val database = RosettaDatabase.getDatabase(application)
|
||||||
private val dialogDao = database.dialogDao()
|
private val dialogDao = database.dialogDao()
|
||||||
private val groupRepository = GroupRepository.getInstance(application)
|
private val uiDeps = UiDependencyAccess.get(application)
|
||||||
|
private val protocolGateway: ProtocolGateway = uiDeps.protocolGateway()
|
||||||
|
private val messageRepository: MessageRepository = uiDeps.messageRepository()
|
||||||
|
private val groupRepository: GroupRepository = uiDeps.groupRepository()
|
||||||
|
|
||||||
private var currentAccount: String = ""
|
private var currentAccount: String = ""
|
||||||
private var currentPrivateKey: String? = null
|
private var currentPrivateKey: String? = null
|
||||||
@@ -215,7 +219,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
}
|
}
|
||||||
if (!dialogName.isNullOrBlank()) return dialogName
|
if (!dialogName.isNullOrBlank()) return dialogName
|
||||||
|
|
||||||
val cached = ProtocolManager.getCachedUserName(publicKey).orEmpty().trim()
|
val cached = protocolGateway.getCachedUserName(publicKey).orEmpty().trim()
|
||||||
if (cached.isNotBlank() && cached != publicKey) {
|
if (cached.isNotBlank() && cached != publicKey) {
|
||||||
return cached
|
return cached
|
||||||
}
|
}
|
||||||
@@ -478,7 +482,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
.getDialogsFlow(publicKey)
|
.getDialogsFlow(publicKey)
|
||||||
.flowOn(Dispatchers.IO) // 🚀 Flow работает на IO
|
.flowOn(Dispatchers.IO) // 🚀 Flow работает на IO
|
||||||
.debounce(100) // 🚀 Батчим быстрые обновления (N сообщений → 1 обработка)
|
.debounce(100) // 🚀 Батчим быстрые обновления (N сообщений → 1 обработка)
|
||||||
.combine(ProtocolManager.syncInProgress) { dialogsList, syncing ->
|
.combine(protocolGateway.syncInProgress) { dialogsList, syncing ->
|
||||||
dialogsList to syncing
|
dialogsList to syncing
|
||||||
}
|
}
|
||||||
.mapLatest { (dialogsList, syncing) ->
|
.mapLatest { (dialogsList, syncing) ->
|
||||||
@@ -529,7 +533,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
.getRequestsFlow(publicKey)
|
.getRequestsFlow(publicKey)
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.debounce(100) // 🚀 Батчим быстрые обновления
|
.debounce(100) // 🚀 Батчим быстрые обновления
|
||||||
.combine(ProtocolManager.syncInProgress) { requestsList, syncing ->
|
.combine(protocolGateway.syncInProgress) { requestsList, syncing ->
|
||||||
requestsList to syncing
|
requestsList to syncing
|
||||||
}
|
}
|
||||||
.mapLatest { (requestsList, syncing) ->
|
.mapLatest { (requestsList, syncing) ->
|
||||||
@@ -553,7 +557,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
dialogDao
|
dialogDao
|
||||||
.getRequestsCountFlow(publicKey)
|
.getRequestsCountFlow(publicKey)
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.combine(ProtocolManager.syncInProgress) { count, syncing ->
|
.combine(protocolGateway.syncInProgress) { count, syncing ->
|
||||||
if (syncing) 0 else count
|
if (syncing) 0 else count
|
||||||
}
|
}
|
||||||
.distinctUntilChanged() // 🔥 ИСПРАВЛЕНИЕ: Игнорируем дублирующиеся значения
|
.distinctUntilChanged() // 🔥 ИСПРАВЛЕНИЕ: Игнорируем дублирующиеся значения
|
||||||
@@ -577,7 +581,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
// dialogs that still have empty titles.
|
// dialogs that still have empty titles.
|
||||||
launch {
|
launch {
|
||||||
var wasSyncing = false
|
var wasSyncing = false
|
||||||
ProtocolManager.syncInProgress.collect { syncing ->
|
protocolGateway.syncInProgress.collect { syncing ->
|
||||||
if (wasSyncing && !syncing) {
|
if (wasSyncing && !syncing) {
|
||||||
requestedUserInfoKeys.clear()
|
requestedUserInfoKeys.clear()
|
||||||
}
|
}
|
||||||
@@ -634,7 +638,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
newKeys.forEach { key -> addPublicKey(key) }
|
newKeys.forEach { key -> addPublicKey(key) }
|
||||||
}
|
}
|
||||||
|
|
||||||
ProtocolManager.send(packet)
|
protocolGateway.send(packet)
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -980,7 +984,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🗑️ 1. Очищаем ВСЕ кэши сообщений
|
// 🗑️ 1. Очищаем ВСЕ кэши сообщений
|
||||||
MessageRepository.getInstance(getApplication()).clearDialogCache(opponentKey)
|
messageRepository.clearDialogCache(opponentKey)
|
||||||
// 🗑️ Очищаем кэш ChatViewModel
|
// 🗑️ Очищаем кэш ChatViewModel
|
||||||
ChatViewModel.clearCacheForOpponent(opponentKey)
|
ChatViewModel.clearCacheForOpponent(opponentKey)
|
||||||
|
|
||||||
@@ -1029,7 +1033,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
_requestsCount.value = _requests.value.size
|
_requestsCount.value = _requests.value.size
|
||||||
dialogsUiCache.remove(groupPublicKey)
|
dialogsUiCache.remove(groupPublicKey)
|
||||||
requestsUiCache.remove(groupPublicKey)
|
requestsUiCache.remove(groupPublicKey)
|
||||||
MessageRepository.getInstance(getApplication()).clearDialogCache(groupPublicKey)
|
messageRepository.clearDialogCache(groupPublicKey)
|
||||||
ChatViewModel.clearCacheForOpponent(groupPublicKey)
|
ChatViewModel.clearCacheForOpponent(groupPublicKey)
|
||||||
}
|
}
|
||||||
left
|
left
|
||||||
@@ -1104,7 +1108,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
this.privateKey = privateKeyHash
|
this.privateKey = privateKeyHash
|
||||||
this.search = publicKey
|
this.search = publicKey
|
||||||
}
|
}
|
||||||
ProtocolManager.send(packet)
|
protocolGateway.send(packet)
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.network.ProtocolState
|
import com.rosetta.messenger.network.ProtocolState
|
||||||
import compose.icons.TablerIcons
|
import compose.icons.TablerIcons
|
||||||
import compose.icons.tablericons.*
|
import compose.icons.tablericons.*
|
||||||
@@ -28,9 +29,12 @@ fun ConnectionLogsScreen(
|
|||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
val logs by ProtocolManager.debugLogs.collectAsState()
|
val context = LocalContext.current
|
||||||
val protocolState by ProtocolManager.getProtocol().state.collectAsState()
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
val syncInProgress by ProtocolManager.syncInProgress.collectAsState()
|
val protocolGateway = remember(uiDeps) { uiDeps.protocolGateway() }
|
||||||
|
val logs by protocolGateway.debugLogs.collectAsState()
|
||||||
|
val protocolState by protocolGateway.state.collectAsState()
|
||||||
|
val syncInProgress by protocolGateway.syncInProgress.collectAsState()
|
||||||
|
|
||||||
val bgColor = if (isDarkTheme) Color(0xFF0E0E0E) else Color(0xFFF5F5F5)
|
val bgColor = if (isDarkTheme) Color(0xFF0E0E0E) else Color(0xFFF5F5F5)
|
||||||
val cardColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color.White
|
val cardColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color.White
|
||||||
@@ -41,9 +45,9 @@ fun ConnectionLogsScreen(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
ProtocolManager.enableUILogs(true)
|
protocolGateway.enableUILogs(true)
|
||||||
onDispose {
|
onDispose {
|
||||||
ProtocolManager.enableUILogs(false)
|
protocolGateway.enableUILogs(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +89,7 @@ fun ConnectionLogsScreen(
|
|||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|
||||||
IconButton(onClick = { ProtocolManager.clearLogs() }) {
|
IconButton(onClick = { protocolGateway.clearLogs() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = TablerIcons.Trash,
|
imageVector = TablerIcons.Trash,
|
||||||
contentDescription = "Clear logs",
|
contentDescription = "Clear logs",
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ import com.rosetta.messenger.data.ForwardManager
|
|||||||
import com.rosetta.messenger.data.GroupRepository
|
import com.rosetta.messenger.data.GroupRepository
|
||||||
import com.rosetta.messenger.data.MessageRepository
|
import com.rosetta.messenger.data.MessageRepository
|
||||||
import com.rosetta.messenger.data.PreferencesManager
|
import com.rosetta.messenger.data.PreferencesManager
|
||||||
|
import com.rosetta.messenger.di.ProtocolGateway
|
||||||
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.database.MessageEntity
|
import com.rosetta.messenger.database.MessageEntity
|
||||||
import com.rosetta.messenger.database.RosettaDatabase
|
import com.rosetta.messenger.database.RosettaDatabase
|
||||||
import com.rosetta.messenger.network.AttachmentType
|
import com.rosetta.messenger.network.AttachmentType
|
||||||
@@ -128,7 +130,6 @@ import com.rosetta.messenger.network.MessageAttachment
|
|||||||
import com.rosetta.messenger.network.OnlineState
|
import com.rosetta.messenger.network.OnlineState
|
||||||
import com.rosetta.messenger.network.PacketOnlineState
|
import com.rosetta.messenger.network.PacketOnlineState
|
||||||
import com.rosetta.messenger.network.PacketOnlineSubscribe
|
import com.rosetta.messenger.network.PacketOnlineSubscribe
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
|
||||||
import com.rosetta.messenger.network.SearchUser
|
import com.rosetta.messenger.network.SearchUser
|
||||||
import com.rosetta.messenger.repository.AvatarRepository
|
import com.rosetta.messenger.repository.AvatarRepository
|
||||||
import com.rosetta.messenger.ui.chats.components.ImageAttachment
|
import com.rosetta.messenger.ui.chats.components.ImageAttachment
|
||||||
@@ -323,6 +324,11 @@ fun GroupInfoScreen(
|
|||||||
onSwipeBackEnabledChanged: (Boolean) -> Unit = {}
|
onSwipeBackEnabledChanged: (Boolean) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val context = androidx.compose.ui.platform.LocalContext.current
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val protocolGateway = remember(uiDeps) { uiDeps.protocolGateway() }
|
||||||
|
val messageRepository = remember(uiDeps) { uiDeps.messageRepository() }
|
||||||
|
val preferencesManager = remember(uiDeps) { uiDeps.preferencesManager() }
|
||||||
|
val groupRepository = remember(uiDeps) { uiDeps.groupRepository() }
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
|
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
|
||||||
@@ -376,9 +382,6 @@ fun GroupInfoScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val groupRepository = remember { GroupRepository.getInstance(context) }
|
|
||||||
val messageRepository = remember { MessageRepository.getInstance(context) }
|
|
||||||
val preferencesManager = remember { PreferencesManager(context) }
|
|
||||||
val database = remember { RosettaDatabase.getDatabase(context) }
|
val database = remember { RosettaDatabase.getDatabase(context) }
|
||||||
val groupDao = remember { database.groupDao() }
|
val groupDao = remember { database.groupDao() }
|
||||||
val messageDao = remember { database.messageDao() }
|
val messageDao = remember { database.messageDao() }
|
||||||
@@ -475,6 +478,7 @@ fun GroupInfoScreen(
|
|||||||
val memberSnapshot = memberInfoByKey.toMap()
|
val memberSnapshot = memberInfoByKey.toMap()
|
||||||
value = withContext(Dispatchers.Default) {
|
value = withContext(Dispatchers.Default) {
|
||||||
buildGroupMediaItems(
|
buildGroupMediaItems(
|
||||||
|
protocolGateway = protocolGateway,
|
||||||
messages = groupMessages,
|
messages = groupMessages,
|
||||||
privateKey = currentUserPrivateKey,
|
privateKey = currentUserPrivateKey,
|
||||||
currentUserPublicKey = currentUserPublicKey,
|
currentUserPublicKey = currentUserPublicKey,
|
||||||
@@ -603,11 +607,11 @@ fun GroupInfoScreen(
|
|||||||
val resolvedUsers = withContext(Dispatchers.IO) {
|
val resolvedUsers = withContext(Dispatchers.IO) {
|
||||||
val resolvedMap = LinkedHashMap<String, SearchUser>()
|
val resolvedMap = LinkedHashMap<String, SearchUser>()
|
||||||
members.forEach { memberKey ->
|
members.forEach { memberKey ->
|
||||||
val cached = ProtocolManager.getCachedUserInfo(memberKey)
|
val cached = protocolGateway.getCachedUserInfo(memberKey)
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
resolvedMap[memberKey] = cached
|
resolvedMap[memberKey] = cached
|
||||||
} else {
|
} else {
|
||||||
ProtocolManager.resolveUserInfo(memberKey, timeoutMs = 2500L)?.let { resolvedUser ->
|
protocolGateway.resolveUserInfo(memberKey, timeoutMs = 2500L)?.let { resolvedUser ->
|
||||||
resolvedMap[memberKey] = resolvedUser
|
resolvedMap[memberKey] = resolvedUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,7 +649,7 @@ fun GroupInfoScreen(
|
|||||||
val resolvedUsers = withContext(Dispatchers.IO) {
|
val resolvedUsers = withContext(Dispatchers.IO) {
|
||||||
val resolvedMap = LinkedHashMap<String, SearchUser>()
|
val resolvedMap = LinkedHashMap<String, SearchUser>()
|
||||||
cached.members.forEach { memberKey ->
|
cached.members.forEach { memberKey ->
|
||||||
ProtocolManager.getCachedUserInfo(memberKey)?.let { resolved ->
|
protocolGateway.getCachedUserInfo(memberKey)?.let { resolved ->
|
||||||
resolvedMap[memberKey] = resolved
|
resolvedMap[memberKey] = resolved
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -685,9 +689,9 @@ fun GroupInfoScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(dialogPublicKey) {
|
DisposableEffect(dialogPublicKey) {
|
||||||
ProtocolManager.waitPacket(0x05, onlinePacketHandler)
|
protocolGateway.waitPacket(0x05, onlinePacketHandler)
|
||||||
onDispose {
|
onDispose {
|
||||||
ProtocolManager.unwaitPacket(0x05, onlinePacketHandler)
|
protocolGateway.unwaitPacket(0x05, onlinePacketHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,7 +709,7 @@ fun GroupInfoScreen(
|
|||||||
this.privateKey = privateKeyHash
|
this.privateKey = privateKeyHash
|
||||||
keysToSubscribe.forEach { addPublicKey(it) }
|
keysToSubscribe.forEach { addPublicKey(it) }
|
||||||
}
|
}
|
||||||
ProtocolManager.send(packet)
|
protocolGateway.send(packet)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
@@ -2357,6 +2361,7 @@ private fun parseAttachmentsForGroupInfo(attachmentsJson: String): List<MessageA
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveGroupSenderName(
|
private fun resolveGroupSenderName(
|
||||||
|
protocolGateway: ProtocolGateway,
|
||||||
senderPublicKey: String,
|
senderPublicKey: String,
|
||||||
currentUserPublicKey: String,
|
currentUserPublicKey: String,
|
||||||
memberInfoByKey: Map<String, SearchUser>
|
memberInfoByKey: Map<String, SearchUser>
|
||||||
@@ -2364,13 +2369,14 @@ private fun resolveGroupSenderName(
|
|||||||
if (senderPublicKey.isBlank()) return "Unknown"
|
if (senderPublicKey.isBlank()) return "Unknown"
|
||||||
if (senderPublicKey == currentUserPublicKey) return "You"
|
if (senderPublicKey == currentUserPublicKey) return "You"
|
||||||
|
|
||||||
val info = memberInfoByKey[senderPublicKey] ?: ProtocolManager.getCachedUserInfo(senderPublicKey)
|
val info = memberInfoByKey[senderPublicKey] ?: protocolGateway.getCachedUserInfo(senderPublicKey)
|
||||||
return info?.title?.takeIf { it.isNotBlank() }
|
return info?.title?.takeIf { it.isNotBlank() }
|
||||||
?: info?.username?.takeIf { it.isNotBlank() }
|
?: info?.username?.takeIf { it.isNotBlank() }
|
||||||
?: shortPublicKey(senderPublicKey)
|
?: shortPublicKey(senderPublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildGroupMediaItems(
|
private fun buildGroupMediaItems(
|
||||||
|
protocolGateway: ProtocolGateway,
|
||||||
messages: List<MessageEntity>,
|
messages: List<MessageEntity>,
|
||||||
privateKey: String,
|
privateKey: String,
|
||||||
currentUserPublicKey: String,
|
currentUserPublicKey: String,
|
||||||
@@ -2389,6 +2395,7 @@ private fun buildGroupMediaItems(
|
|||||||
?: if (message.fromMe == 1) currentUserPublicKey else ""
|
?: if (message.fromMe == 1) currentUserPublicKey else ""
|
||||||
val senderCacheKey = senderKey.ifBlank { currentUserPublicKey }
|
val senderCacheKey = senderKey.ifBlank { currentUserPublicKey }
|
||||||
val senderName = resolveGroupSenderName(
|
val senderName = resolveGroupSenderName(
|
||||||
|
protocolGateway = protocolGateway,
|
||||||
senderPublicKey = senderCacheKey,
|
senderPublicKey = senderCacheKey,
|
||||||
currentUserPublicKey = currentUserPublicKey,
|
currentUserPublicKey = currentUserPublicKey,
|
||||||
memberInfoByKey = memberInfoByKey
|
memberInfoByKey = memberInfoByKey
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import androidx.core.view.WindowInsetsCompat
|
|||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.rosetta.messenger.R
|
import com.rosetta.messenger.R
|
||||||
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.data.GroupRepository
|
import com.rosetta.messenger.data.GroupRepository
|
||||||
import com.rosetta.messenger.data.MessageRepository
|
import com.rosetta.messenger.data.MessageRepository
|
||||||
import com.rosetta.messenger.database.DialogDao
|
import com.rosetta.messenger.database.DialogDao
|
||||||
@@ -122,6 +123,9 @@ fun GroupSetupScreen(
|
|||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val messageRepository = remember(uiDeps) { uiDeps.messageRepository() }
|
||||||
|
val groupRepository = remember(uiDeps) { uiDeps.groupRepository() }
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
@@ -255,7 +259,7 @@ fun GroupSetupScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createGroup() =
|
suspend fun createGroup() =
|
||||||
GroupRepository.getInstance(context).createGroup(
|
groupRepository.createGroup(
|
||||||
accountPublicKey = accountPublicKey,
|
accountPublicKey = accountPublicKey,
|
||||||
accountPrivateKey = accountPrivateKey,
|
accountPrivateKey = accountPrivateKey,
|
||||||
title = title.trim(),
|
title = title.trim(),
|
||||||
@@ -1091,23 +1095,21 @@ fun GroupSetupScreen(
|
|||||||
// Send invite to all selected members
|
// Send invite to all selected members
|
||||||
if (selectedMembers.isNotEmpty()) {
|
if (selectedMembers.isNotEmpty()) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val groupRepo = GroupRepository.getInstance(context)
|
val groupKey = groupRepository.getGroupKey(
|
||||||
val groupKey = groupRepo.getGroupKey(
|
|
||||||
accountPublicKey, accountPrivateKey,
|
accountPublicKey, accountPrivateKey,
|
||||||
result.dialogPublicKey
|
result.dialogPublicKey
|
||||||
)
|
)
|
||||||
if (!groupKey.isNullOrBlank()) {
|
if (!groupKey.isNullOrBlank()) {
|
||||||
val invite = groupRepo.constructInviteString(
|
val invite = groupRepository.constructInviteString(
|
||||||
groupId = result.dialogPublicKey,
|
groupId = result.dialogPublicKey,
|
||||||
title = result.title.ifBlank { title.trim() },
|
title = result.title.ifBlank { title.trim() },
|
||||||
encryptKey = groupKey,
|
encryptKey = groupKey,
|
||||||
description = description.trim()
|
description = description.trim()
|
||||||
)
|
)
|
||||||
if (invite.isNotBlank()) {
|
if (invite.isNotBlank()) {
|
||||||
val msgRepo = MessageRepository.getInstance(context)
|
|
||||||
selectedMembers.forEach { member ->
|
selectedMembers.forEach { member ->
|
||||||
runCatching {
|
runCatching {
|
||||||
msgRepo.sendMessage(
|
messageRepository.sendMessage(
|
||||||
toPublicKey = member.publicKey,
|
toPublicKey = member.publicKey,
|
||||||
text = invite
|
text = invite
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.network.SearchUser
|
import com.rosetta.messenger.network.SearchUser
|
||||||
import com.rosetta.messenger.repository.AvatarRepository
|
import com.rosetta.messenger.repository.AvatarRepository
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
@@ -39,8 +41,11 @@ fun RequestsListScreen(
|
|||||||
onUserSelect: (SearchUser) -> Unit,
|
onUserSelect: (SearchUser) -> Unit,
|
||||||
avatarRepository: AvatarRepository? = null
|
avatarRepository: AvatarRepository? = null
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val protocolGateway = remember(uiDeps) { uiDeps.protocolGateway() }
|
||||||
val chatsState by chatsViewModel.chatsState.collectAsState()
|
val chatsState by chatsViewModel.chatsState.collectAsState()
|
||||||
val syncInProgress by ProtocolManager.syncInProgress.collectAsState()
|
val syncInProgress by protocolGateway.syncInProgress.collectAsState()
|
||||||
val requests = if (syncInProgress) emptyList() else chatsState.requests
|
val requests = if (syncInProgress) emptyList() else chatsState.requests
|
||||||
val blockedUsers by chatsViewModel.blockedUsers.collectAsState()
|
val blockedUsers by chatsViewModel.blockedUsers.collectAsState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.airbnb.lottie.compose.*
|
import com.airbnb.lottie.compose.*
|
||||||
import com.rosetta.messenger.R
|
import com.rosetta.messenger.R
|
||||||
import com.rosetta.messenger.data.AccountManager
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.data.RecentSearchesManager
|
import com.rosetta.messenger.data.RecentSearchesManager
|
||||||
import com.rosetta.messenger.data.isPlaceholderAccountName
|
import com.rosetta.messenger.data.isPlaceholderAccountName
|
||||||
import com.rosetta.messenger.network.ProtocolState
|
import com.rosetta.messenger.network.ProtocolState
|
||||||
@@ -104,6 +104,8 @@ fun SearchScreen(
|
|||||||
) {
|
) {
|
||||||
// Context и View для мгновенного закрытия клавиатуры
|
// Context и View для мгновенного закрытия клавиатуры
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val accountManager = remember(uiDeps) { uiDeps.accountManager() }
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
if (!view.isInEditMode) {
|
if (!view.isInEditMode) {
|
||||||
@@ -173,7 +175,7 @@ fun SearchScreen(
|
|||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
val account = AccountManager(context).getAccount(currentUserPublicKey)
|
val account = accountManager.getAccount(currentUserPublicKey)
|
||||||
ownAccountName = account?.name?.trim().orEmpty()
|
ownAccountName = account?.name?.trim().orEmpty()
|
||||||
ownAccountUsername = account?.username?.trim().orEmpty()
|
ownAccountUsername = account?.username?.trim().orEmpty()
|
||||||
}
|
}
|
||||||
@@ -993,6 +995,8 @@ private fun MessagesTabContent(
|
|||||||
onUserSelect: (SearchUser) -> Unit
|
onUserSelect: (SearchUser) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val messageRepository = remember(uiDeps) { uiDeps.messageRepository() }
|
||||||
var results by remember { mutableStateOf<List<MessageSearchResult>>(emptyList()) }
|
var results by remember { mutableStateOf<List<MessageSearchResult>>(emptyList()) }
|
||||||
var isSearching by remember { mutableStateOf(false) }
|
var isSearching by remember { mutableStateOf(false) }
|
||||||
val dividerColor = remember(isDarkTheme) {
|
val dividerColor = remember(isDarkTheme) {
|
||||||
@@ -1021,8 +1025,7 @@ private fun MessagesTabContent(
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val db = RosettaDatabase.getDatabase(context)
|
val db = RosettaDatabase.getDatabase(context)
|
||||||
val repo = com.rosetta.messenger.data.MessageRepository.getInstance(context)
|
val privateKey = messageRepository.getCurrentPrivateKey().orEmpty()
|
||||||
val privateKey = repo.getCurrentPrivateKey().orEmpty()
|
|
||||||
if (privateKey.isBlank()) {
|
if (privateKey.isBlank()) {
|
||||||
isSearching = false
|
isSearching = false
|
||||||
return@withContext
|
return@withContext
|
||||||
@@ -1481,11 +1484,13 @@ private fun MediaTabContent(
|
|||||||
onOpenImageViewer: (images: List<com.rosetta.messenger.ui.chats.components.ViewableImage>, initialIndex: Int, privateKey: String) -> Unit = { _, _, _ -> }
|
onOpenImageViewer: (images: List<com.rosetta.messenger.ui.chats.components.ViewableImage>, initialIndex: Int, privateKey: String) -> Unit = { _, _, _ -> }
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val messageRepository = remember(uiDeps) { uiDeps.messageRepository() }
|
||||||
var mediaItems by remember { mutableStateOf<List<MediaItem>>(emptyList()) }
|
var mediaItems by remember { mutableStateOf<List<MediaItem>>(emptyList()) }
|
||||||
var isLoading by remember { mutableStateOf(true) }
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
val privateKey = remember {
|
val privateKey = remember {
|
||||||
com.rosetta.messenger.data.MessageRepository.getInstance(context).getCurrentPrivateKey().orEmpty()
|
messageRepository.getCurrentPrivateKey().orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
val viewerImages = remember(mediaItems) {
|
val viewerImages = remember(mediaItems) {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.rosetta.messenger.ui.chats
|
package com.rosetta.messenger.ui.chats
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.rosetta.messenger.di.ProtocolGateway
|
||||||
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.network.PacketSearch
|
import com.rosetta.messenger.network.PacketSearch
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
|
||||||
import com.rosetta.messenger.network.SearchUser
|
import com.rosetta.messenger.network.SearchUser
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -20,7 +22,9 @@ import kotlinx.coroutines.launch
|
|||||||
* ViewModel для поиска пользователей через протокол
|
* ViewModel для поиска пользователей через протокол
|
||||||
* Работает аналогично SearchBar в React Native приложении
|
* Работает аналогично SearchBar в React Native приложении
|
||||||
*/
|
*/
|
||||||
class SearchUsersViewModel : ViewModel() {
|
class SearchUsersViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
private val protocolGateway: ProtocolGateway =
|
||||||
|
UiDependencyAccess.get(application).protocolGateway()
|
||||||
|
|
||||||
// Состояние поиска
|
// Состояние поиска
|
||||||
private val _searchQuery = MutableStateFlow("")
|
private val _searchQuery = MutableStateFlow("")
|
||||||
@@ -47,7 +51,7 @@ class SearchUsersViewModel : ViewModel() {
|
|||||||
init {
|
init {
|
||||||
packetFlowJob =
|
packetFlowJob =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
ProtocolManager.packetFlow(0x03).collectLatest { packet ->
|
protocolGateway.packetFlow(0x03).collectLatest { packet ->
|
||||||
val searchPacket = packet as? PacketSearch ?: return@collectLatest
|
val searchPacket = packet as? PacketSearch ?: return@collectLatest
|
||||||
logSearch(
|
logSearch(
|
||||||
"📥 PacketSearch response: search='${searchPacket.search}', users=${searchPacket.users.size}"
|
"📥 PacketSearch response: search='${searchPacket.search}', users=${searchPacket.users.size}"
|
||||||
@@ -118,7 +122,7 @@ class SearchUsersViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val effectivePrivateHash =
|
val effectivePrivateHash =
|
||||||
privateKeyHash.ifBlank { ProtocolManager.getProtocol().getPrivateHash().orEmpty() }
|
privateKeyHash.ifBlank { protocolGateway.getPrivateHash().orEmpty() }
|
||||||
if (effectivePrivateHash.isBlank()) {
|
if (effectivePrivateHash.isBlank()) {
|
||||||
_isSearching.value = false
|
_isSearching.value = false
|
||||||
logSearch("❌ Skip send: private hash is empty")
|
logSearch("❌ Skip send: private hash is empty")
|
||||||
@@ -131,7 +135,7 @@ class SearchUsersViewModel : ViewModel() {
|
|||||||
this.search = normalizedQuery
|
this.search = normalizedQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
ProtocolManager.sendPacket(packetSearch)
|
protocolGateway.sendPacket(packetSearch)
|
||||||
logSearch("📤 PacketSearch sent: '$normalizedQuery'")
|
logSearch("📤 PacketSearch sent: '$normalizedQuery'")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import com.rosetta.messenger.data.GroupRepository
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.network.AttachmentType
|
import com.rosetta.messenger.network.AttachmentType
|
||||||
import com.rosetta.messenger.network.GroupStatus
|
import com.rosetta.messenger.network.GroupStatus
|
||||||
import com.rosetta.messenger.network.MessageAttachment
|
import com.rosetta.messenger.network.MessageAttachment
|
||||||
@@ -1693,7 +1693,8 @@ private fun GroupInviteInlineCard(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val groupRepository = remember { GroupRepository.getInstance(context) }
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val groupRepository = remember(uiDeps) { uiDeps.groupRepository() }
|
||||||
|
|
||||||
val normalizedInvite = remember(inviteText) { inviteText.trim() }
|
val normalizedInvite = remember(inviteText) { inviteText.trim() }
|
||||||
val parsedInvite = remember(normalizedInvite) { groupRepository.parseInviteString(normalizedInvite) }
|
val parsedInvite = remember(normalizedInvite) { groupRepository.parseInviteString(normalizedInvite) }
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ import androidx.compose.animation.core.Animatable
|
|||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import com.rosetta.messenger.data.PreferencesManager
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 📷 In-App Camera Screen - как в Telegram
|
* 📷 In-App Camera Screen - как в Telegram
|
||||||
@@ -91,9 +91,8 @@ fun InAppCameraScreen(
|
|||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val preferencesManager = remember(context.applicationContext) {
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
PreferencesManager(context.applicationContext)
|
val preferencesManager = remember(uiDeps) { uiDeps.preferencesManager() }
|
||||||
}
|
|
||||||
|
|
||||||
// Camera state
|
// Camera state
|
||||||
var lensFacing by remember { mutableStateOf(CameraSelector.LENS_FACING_BACK) }
|
var lensFacing by remember { mutableStateOf(CameraSelector.LENS_FACING_BACK) }
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import compose.icons.TablerIcons
|
|||||||
import compose.icons.tablericons.ChevronLeft
|
import compose.icons.tablericons.ChevronLeft
|
||||||
import com.rosetta.messenger.R
|
import com.rosetta.messenger.R
|
||||||
import com.rosetta.messenger.data.PreferencesManager
|
import com.rosetta.messenger.data.PreferencesManager
|
||||||
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -56,7 +57,8 @@ fun AppIconScreen(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val prefs = remember { PreferencesManager(context) }
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val prefs = remember(uiDeps) { uiDeps.preferencesManager() }
|
||||||
var currentIcon by remember { mutableStateOf("default") }
|
var currentIcon by remember { mutableStateOf("default") }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.rosetta.messenger.data.PreferencesManager
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.ui.icons.TelegramIcons
|
import com.rosetta.messenger.ui.icons.TelegramIcons
|
||||||
import compose.icons.TablerIcons
|
import compose.icons.TablerIcons
|
||||||
import compose.icons.tablericons.ChevronLeft
|
import compose.icons.tablericons.ChevronLeft
|
||||||
@@ -40,7 +40,8 @@ fun NotificationsScreen(
|
|||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val preferencesManager = remember { PreferencesManager(context) }
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val preferencesManager = remember(uiDeps) { uiDeps.preferencesManager() }
|
||||||
val notificationsEnabled by preferencesManager.notificationsEnabled.collectAsState(initial = true)
|
val notificationsEnabled by preferencesManager.notificationsEnabled.collectAsState(initial = true)
|
||||||
val avatarInNotifications by preferencesManager.notificationAvatarEnabled.collectAsState(initial = true)
|
val avatarInNotifications by preferencesManager.notificationAvatarEnabled.collectAsState(initial = true)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ import com.rosetta.messenger.R
|
|||||||
import com.rosetta.messenger.crypto.CryptoManager
|
import com.rosetta.messenger.crypto.CryptoManager
|
||||||
import com.rosetta.messenger.crypto.MessageCrypto
|
import com.rosetta.messenger.crypto.MessageCrypto
|
||||||
import com.rosetta.messenger.data.MessageRepository
|
import com.rosetta.messenger.data.MessageRepository
|
||||||
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.database.MessageEntity
|
import com.rosetta.messenger.database.MessageEntity
|
||||||
import com.rosetta.messenger.database.RosettaDatabase
|
import com.rosetta.messenger.database.RosettaDatabase
|
||||||
import com.rosetta.messenger.network.AttachmentType
|
import com.rosetta.messenger.network.AttachmentType
|
||||||
@@ -255,7 +256,8 @@ fun OtherProfileScreen(
|
|||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
// 🔕 Mute state
|
// 🔕 Mute state
|
||||||
val preferencesManager = remember { com.rosetta.messenger.data.PreferencesManager(context) }
|
val uiDeps = remember(context) { UiDependencyAccess.get(context) }
|
||||||
|
val preferencesManager = remember(uiDeps) { uiDeps.preferencesManager() }
|
||||||
var notificationsEnabled by remember { mutableStateOf(true) }
|
var notificationsEnabled by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
// 🔥 Загружаем статус блокировки при открытии экрана
|
// 🔥 Загружаем статус блокировки при открытии экрана
|
||||||
@@ -356,7 +358,7 @@ fun OtherProfileScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// <20>🟢 Наблюдаем за онлайн статусом пользователя в реальном времени
|
// <20>🟢 Наблюдаем за онлайн статусом пользователя в реальном времени
|
||||||
val messageRepository = remember { MessageRepository.getInstance(context) }
|
val messageRepository = remember(uiDeps) { uiDeps.messageRepository() }
|
||||||
val onlineStatus by
|
val onlineStatus by
|
||||||
messageRepository
|
messageRepository
|
||||||
.observeUserOnlineStatus(user.publicKey)
|
.observeUserOnlineStatus(user.publicKey)
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import android.app.Application
|
|||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.rosetta.messenger.data.AccountManager
|
import com.rosetta.messenger.data.AccountManager
|
||||||
|
import com.rosetta.messenger.di.ProtocolGateway
|
||||||
|
import com.rosetta.messenger.di.UiDependencyAccess
|
||||||
import com.rosetta.messenger.data.EncryptedAccount
|
import com.rosetta.messenger.data.EncryptedAccount
|
||||||
import com.rosetta.messenger.network.PacketResult
|
import com.rosetta.messenger.network.PacketResult
|
||||||
import com.rosetta.messenger.network.PacketUserInfo
|
import com.rosetta.messenger.network.PacketUserInfo
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@@ -24,7 +25,9 @@ data class ProfileState(
|
|||||||
|
|
||||||
class ProfileViewModel(application: Application) : AndroidViewModel(application) {
|
class ProfileViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
private val accountManager = AccountManager(application)
|
private val uiDeps = UiDependencyAccess.get(application)
|
||||||
|
private val accountManager: AccountManager = uiDeps.accountManager()
|
||||||
|
private val protocolGateway: ProtocolGateway = uiDeps.protocolGateway()
|
||||||
|
|
||||||
private val _state = MutableStateFlow(ProfileState())
|
private val _state = MutableStateFlow(ProfileState())
|
||||||
val state: StateFlow<ProfileState> = _state
|
val state: StateFlow<ProfileState> = _state
|
||||||
@@ -34,7 +37,7 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
init {
|
init {
|
||||||
packetFlowJob =
|
packetFlowJob =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
ProtocolManager.packetFlow(0x02).collectLatest { packet ->
|
protocolGateway.packetFlow(0x02).collectLatest { packet ->
|
||||||
val result = packet as? PacketResult ?: return@collectLatest
|
val result = packet as? PacketResult ?: return@collectLatest
|
||||||
handlePacketResult(result)
|
handlePacketResult(result)
|
||||||
}
|
}
|
||||||
@@ -98,7 +101,7 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
// CRITICAL: Log full packet data for debugging
|
// CRITICAL: Log full packet data for debugging
|
||||||
|
|
||||||
addLog("Sending packet to server...")
|
addLog("Sending packet to server...")
|
||||||
ProtocolManager.send(packet)
|
protocolGateway.send(packet)
|
||||||
addLog("Packet sent successfully")
|
addLog("Packet sent successfully")
|
||||||
addLog("Waiting for PacketResult (0x02) from server...")
|
addLog("Waiting for PacketResult (0x02) from server...")
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ plugins {
|
|||||||
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
|
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
|
||||||
id("com.google.devtools.ksp") version "1.9.20-1.0.14" apply false
|
id("com.google.devtools.ksp") version "1.9.20-1.0.14" apply false
|
||||||
id("com.google.gms.google-services") version "4.4.0" apply false
|
id("com.google.gms.google-services") version "4.4.0" apply false
|
||||||
|
id("com.google.dagger.hilt.android") version "2.51.1" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("clean", Delete::class) {
|
tasks.register("clean", Delete::class) {
|
||||||
|
|||||||
Reference in New Issue
Block a user