Доработки интерфейса и поведения настроек, профиля и групп

This commit is contained in:
2026-04-06 21:41:37 +05:00
parent 152106eda1
commit 081bdb6d30
26 changed files with 639 additions and 161 deletions

View File

@@ -16,10 +16,15 @@ import androidx.activity.compose.BackHandler
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -67,6 +72,7 @@ import com.rosetta.messenger.ui.crashlogs.CrashLogsScreen
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
import com.rosetta.messenger.ui.settings.BackupScreen
import com.rosetta.messenger.ui.settings.BiometricEnableScreen
import com.rosetta.messenger.ui.settings.NotificationsScreen
import com.rosetta.messenger.ui.settings.OtherProfileScreen
import com.rosetta.messenger.ui.settings.ProfileScreen
import com.rosetta.messenger.ui.settings.SafetyScreen
@@ -203,6 +209,8 @@ class MainActivity : FragmentActivity() {
var currentAccount by remember { mutableStateOf(getCachedSessionAccount()) }
var accountInfoList by remember { mutableStateOf<List<AccountInfo>>(emptyList()) }
var startCreateAccountFlow by remember { mutableStateOf(false) }
var preservedMainNavStack by remember { mutableStateOf<List<Screen>>(emptyList()) }
var preservedMainNavAccountKey by remember { mutableStateOf("") }
// Check for existing accounts and build AccountInfo list
LaunchedEffect(Unit) {
@@ -303,6 +311,8 @@ class MainActivity : FragmentActivity() {
},
onLogout = {
startCreateAccountFlow = false
preservedMainNavStack = emptyList()
preservedMainNavAccountKey = ""
// Set currentAccount to null immediately to prevent UI
// lag
currentAccount = null
@@ -316,8 +326,27 @@ class MainActivity : FragmentActivity() {
)
}
"main" -> {
val activeAccountKey = currentAccount?.publicKey.orEmpty()
MainScreen(
account = currentAccount,
initialNavStack =
if (
activeAccountKey.isNotBlank() &&
preservedMainNavAccountKey.equals(
activeAccountKey,
ignoreCase = true
)
) {
preservedMainNavStack
} else {
emptyList()
},
onNavStackChanged = { stack ->
if (activeAccountKey.isNotBlank()) {
preservedMainNavAccountKey = activeAccountKey
preservedMainNavStack = stack
}
},
isDarkTheme = isDarkTheme,
themeMode = themeMode,
onToggleTheme = {
@@ -331,6 +360,8 @@ class MainActivity : FragmentActivity() {
},
onLogout = {
startCreateAccountFlow = false
preservedMainNavStack = emptyList()
preservedMainNavAccountKey = ""
// Set currentAccount to null immediately to prevent UI
// lag
currentAccount = null
@@ -343,6 +374,8 @@ class MainActivity : FragmentActivity() {
},
onDeleteAccount = {
startCreateAccountFlow = false
preservedMainNavStack = emptyList()
preservedMainNavAccountKey = ""
val publicKey = currentAccount?.publicKey ?: return@MainScreen
scope.launch {
try {
@@ -405,6 +438,8 @@ class MainActivity : FragmentActivity() {
hasExistingAccount = accounts.isNotEmpty()
// 9. If current account is deleted, return to main login screen
if (currentAccount?.publicKey == targetPublicKey) {
preservedMainNavStack = emptyList()
preservedMainNavAccountKey = ""
currentAccount = null
clearCachedSessionAccount()
}
@@ -415,6 +450,8 @@ class MainActivity : FragmentActivity() {
},
onSwitchAccount = { targetPublicKey ->
startCreateAccountFlow = false
preservedMainNavStack = emptyList()
preservedMainNavAccountKey = ""
// Save target account before leaving main screen so Unlock
// screen preselects the account the user tapped.
accountManager.setLastLoggedPublicKey(targetPublicKey)
@@ -429,6 +466,8 @@ class MainActivity : FragmentActivity() {
},
onAddAccount = {
startCreateAccountFlow = true
preservedMainNavStack = emptyList()
preservedMainNavAccountKey = ""
currentAccount = null
clearCachedSessionAccount()
scope.launch {
@@ -442,6 +481,8 @@ class MainActivity : FragmentActivity() {
DeviceConfirmScreen(
isDarkTheme = isDarkTheme,
onExit = {
preservedMainNavStack = emptyList()
preservedMainNavAccountKey = ""
currentAccount = null
clearCachedSessionAccount()
scope.launch {
@@ -614,6 +655,7 @@ private fun EncryptedAccount.toAccountInfo(): AccountInfo {
sealed class Screen {
data object Profile : Screen()
data object ProfileFromChat : Screen()
data object Notifications : Screen()
data object Requests : Screen()
data object Search : Screen()
data object GroupSetup : Screen()
@@ -634,6 +676,8 @@ sealed class Screen {
@Composable
fun MainScreen(
account: DecryptedAccount? = null,
initialNavStack: List<Screen> = emptyList(),
onNavStackChanged: (List<Screen>) -> Unit = {},
isDarkTheme: Boolean = true,
themeMode: String = "dark",
onToggleTheme: () -> Unit = {},
@@ -941,7 +985,8 @@ fun MainScreen(
// navigation change. This eliminates the massive recomposition
// that happened when ANY boolean flag changed.
// ═══════════════════════════════════════════════════════════
var navStack by remember { mutableStateOf<List<Screen>>(emptyList()) }
var navStack by remember(accountPublicKey) { mutableStateOf(initialNavStack) }
LaunchedEffect(navStack) { onNavStackChanged(navStack) }
// Derived visibility — only triggers recomposition when THIS screen changes
val isProfileVisible by remember { derivedStateOf { navStack.any { it is Screen.Profile } } }
@@ -949,6 +994,9 @@ fun MainScreen(
derivedStateOf { navStack.any { it is Screen.ProfileFromChat } }
}
val isRequestsVisible by remember { derivedStateOf { navStack.any { it is Screen.Requests } } }
val isNotificationsVisible by remember {
derivedStateOf { navStack.any { it is Screen.Notifications } }
}
val isSearchVisible by remember { derivedStateOf { navStack.any { it is Screen.Search } } }
val isGroupSetupVisible by remember { derivedStateOf { navStack.any { it is Screen.GroupSetup } } }
val chatDetailScreen by remember {
@@ -980,6 +1028,9 @@ fun MainScreen(
val isAppearanceVisible by remember {
derivedStateOf { navStack.any { it is Screen.Appearance } }
}
var profileHasUnsavedChanges by remember(accountPublicKey) { mutableStateOf(false) }
var showDiscardProfileChangesDialog by remember { mutableStateOf(false) }
var discardProfileChangesFromChat by remember { mutableStateOf(false) }
// Navigation helpers
fun pushScreen(screen: Screen) {
@@ -1027,7 +1078,8 @@ fun MainScreen(
navStack =
navStack.filterNot {
it is Screen.Profile ||
it is Screen.Theme ||
it is Screen.Theme ||
it is Screen.Notifications ||
it is Screen.Safety ||
it is Screen.Backup ||
it is Screen.Logs ||
@@ -1036,6 +1088,21 @@ fun MainScreen(
it is Screen.Appearance
}
}
fun performProfileBack(fromChat: Boolean) {
if (fromChat) {
navStack = navStack.filterNot { it is Screen.ProfileFromChat }
} else {
popProfileAndChildren()
}
}
fun requestProfileBack(fromChat: Boolean) {
if (profileHasUnsavedChanges) {
discardProfileChangesFromChat = fromChat
showDiscardProfileChangesDialog = true
} else {
performProfileBack(fromChat)
}
}
fun popChatAndChildren() {
navStack =
navStack.filterNot {
@@ -1043,6 +1110,13 @@ fun MainScreen(
}
}
LaunchedEffect(isProfileVisible, isProfileFromChatVisible) {
if (!isProfileVisible && !isProfileFromChatVisible) {
profileHasUnsavedChanges = false
showDiscardProfileChangesDialog = false
}
}
// ProfileViewModel для логов
val profileViewModel: com.rosetta.messenger.ui.settings.ProfileViewModel =
androidx.lifecycle.viewmodel.compose.viewModel()
@@ -1196,9 +1270,10 @@ fun MainScreen(
// ═══════════════════════════════════════════════════════════
SwipeBackContainer(
isVisible = isProfileVisible,
onBack = { popProfileAndChildren() },
onBack = { requestProfileBack(fromChat = false) },
isDarkTheme = isDarkTheme,
layer = 1,
swipeEnabled = !profileHasUnsavedChanges,
propagateBackgroundProgress = false
) {
// Экран профиля
@@ -1209,13 +1284,15 @@ fun MainScreen(
accountVerified = accountVerified,
accountPublicKey = accountPublicKey,
accountPrivateKeyHash = privateKeyHash,
onBack = { popProfileAndChildren() },
onBack = { requestProfileBack(fromChat = false) },
onHasChangesChanged = { profileHasUnsavedChanges = it },
onSaveProfile = { name, username ->
accountName = name
accountUsername = username
mainScreenScope.launch { onAccountInfoUpdated() }
},
onLogout = onLogout,
onNavigateToNotifications = { pushScreen(Screen.Notifications) },
onNavigateToTheme = { pushScreen(Screen.Theme) },
onNavigateToAppearance = { pushScreen(Screen.Appearance) },
onNavigateToSafety = { pushScreen(Screen.Safety) },
@@ -1229,6 +1306,18 @@ fun MainScreen(
}
// Other screens with swipe back
SwipeBackContainer(
isVisible = isNotificationsVisible,
onBack = { navStack = navStack.filterNot { it is Screen.Notifications } },
isDarkTheme = isDarkTheme,
layer = 2
) {
NotificationsScreen(
isDarkTheme = isDarkTheme,
onBack = { navStack = navStack.filterNot { it is Screen.Notifications } }
)
}
SwipeBackContainer(
isVisible = isSafetyVisible,
onBack = { navStack = navStack.filterNot { it is Screen.Safety } },
@@ -1420,9 +1509,10 @@ fun MainScreen(
SwipeBackContainer(
isVisible = isProfileFromChatVisible,
onBack = { navStack = navStack.filterNot { it is Screen.ProfileFromChat } },
onBack = { requestProfileBack(fromChat = true) },
isDarkTheme = isDarkTheme,
layer = 1,
swipeEnabled = !profileHasUnsavedChanges,
propagateBackgroundProgress = false
) {
ProfileScreen(
@@ -1432,13 +1522,15 @@ fun MainScreen(
accountVerified = accountVerified,
accountPublicKey = accountPublicKey,
accountPrivateKeyHash = privateKeyHash,
onBack = { navStack = navStack.filterNot { it is Screen.ProfileFromChat } },
onBack = { requestProfileBack(fromChat = true) },
onHasChangesChanged = { profileHasUnsavedChanges = it },
onSaveProfile = { name, username ->
accountName = name
accountUsername = username
mainScreenScope.launch { onAccountInfoUpdated() }
},
onLogout = onLogout,
onNavigateToNotifications = { pushScreen(Screen.Notifications) },
onNavigateToTheme = { pushScreen(Screen.Theme) },
onNavigateToAppearance = { pushScreen(Screen.Appearance) },
onNavigateToSafety = { pushScreen(Screen.Safety) },
@@ -1690,6 +1782,21 @@ fun MainScreen(
)
}
if (isCallScreenVisible) {
// Блокируем любой ввод по нижележащим экранам, пока открыт полноэкранный CallOverlay.
// Иначе тапы могут "пробивать" в чат (иконка звонка, kebab, input и т.д.).
val blockerInteraction = remember { MutableInteractionSource() }
Box(
modifier = Modifier
.fillMaxSize()
.clickable(
interactionSource = blockerInteraction,
indication = null,
onClick = {}
)
)
}
CallOverlay(
state = callUiState,
isDarkTheme = isDarkTheme,
@@ -1706,5 +1813,43 @@ fun MainScreen(
}
}
)
if (showDiscardProfileChangesDialog) {
AlertDialog(
onDismissRequest = { showDiscardProfileChangesDialog = false },
containerColor = if (isDarkTheme) Color(0xFF1E1E20) else Color.White,
title = {
Text(
text = "Discard changes?",
color = if (isDarkTheme) Color.White else Color.Black
)
},
text = {
Text(
text = "You have unsaved profile changes. If you leave now, they will be lost.",
color = if (isDarkTheme) Color(0xFFB5B5BC) else Color(0xFF5A5A66)
)
},
confirmButton = {
TextButton(
onClick = {
showDiscardProfileChangesDialog = false
profileHasUnsavedChanges = false
performProfileBack(discardProfileChangesFromChat)
}
) {
Text("Discard", color = Color(0xFFFF3B30))
}
},
dismissButton = {
TextButton(onClick = { showDiscardProfileChangesDialog = false }) {
Text(
"Stay",
color = if (isDarkTheme) Color(0xFF5FA8FF) else Color(0xFF0D8CF4)
)
}
}
)
}
}
}