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