feat: simplify color handling in ChatsListScreen and improve gesture callbacks in SwipeableDialogItem
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
<application
|
||||
android:name=".RosettaApplication"
|
||||
android:allowBackup="true"
|
||||
android:enableOnBackInvokedCallback="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
||||
@@ -331,6 +331,56 @@ class MainActivity : FragmentActivity() {
|
||||
accountManager.logout()
|
||||
}
|
||||
},
|
||||
onDeleteAccount = {
|
||||
val publicKey = currentAccount?.publicKey ?: return@MainScreen
|
||||
scope.launch {
|
||||
try {
|
||||
val database = RosettaDatabase.getDatabase(this@MainActivity)
|
||||
// 1. Delete all messages
|
||||
database.messageDao().deleteAllByAccount(publicKey)
|
||||
// 2. Delete all dialogs
|
||||
database.dialogDao().deleteAllByAccount(publicKey)
|
||||
// 3. Delete blacklist
|
||||
database.blacklistDao().deleteAllByAccount(publicKey)
|
||||
// 4. Delete avatars from DB
|
||||
database.avatarDao().deleteAvatars(publicKey)
|
||||
// 5. Delete account from Room DB
|
||||
database.accountDao().deleteAccount(publicKey)
|
||||
// 6. Disconnect protocol
|
||||
com.rosetta.messenger.network.ProtocolManager.disconnect()
|
||||
// 7. Delete account from AccountManager DataStore (removes from accounts list + clears login)
|
||||
accountManager.deleteAccount(publicKey)
|
||||
// 8. Refresh accounts list
|
||||
val accounts = accountManager.getAllAccounts()
|
||||
accountInfoList = accounts.map { acc ->
|
||||
val shortKey = acc.publicKey.take(7)
|
||||
val displayName = acc.name ?: shortKey
|
||||
val initials = displayName.trim()
|
||||
.split(Regex("\\s+"))
|
||||
.filter { it.isNotEmpty() }
|
||||
.let { words ->
|
||||
when {
|
||||
words.isEmpty() -> "??"
|
||||
words.size == 1 -> words[0].take(2).uppercase()
|
||||
else -> "${words[0].first()}${words[1].first()}".uppercase()
|
||||
}
|
||||
}
|
||||
AccountInfo(
|
||||
id = acc.publicKey,
|
||||
name = displayName,
|
||||
username = acc.username ?: "",
|
||||
initials = initials,
|
||||
publicKey = acc.publicKey
|
||||
)
|
||||
}
|
||||
hasExistingAccount = accounts.isNotEmpty()
|
||||
// 8. Navigate away last
|
||||
currentAccount = null
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("DeleteAccount", "Failed to delete account", e)
|
||||
}
|
||||
}
|
||||
},
|
||||
onAccountInfoUpdated = {
|
||||
// Reload account list when profile is updated
|
||||
val accounts = accountManager.getAllAccounts()
|
||||
@@ -509,6 +559,7 @@ fun MainScreen(
|
||||
onToggleTheme: () -> Unit = {},
|
||||
onThemeModeChange: (String) -> Unit = {},
|
||||
onLogout: () -> Unit = {},
|
||||
onDeleteAccount: () -> Unit = {},
|
||||
onAccountInfoUpdated: suspend () -> Unit = {}
|
||||
) {
|
||||
// Reactive state for account name and username
|
||||
@@ -758,7 +809,6 @@ fun MainScreen(
|
||||
onNavigateToAppearance = { pushScreen(Screen.Appearance) },
|
||||
onNavigateToSafety = { pushScreen(Screen.Safety) },
|
||||
onNavigateToLogs = { pushScreen(Screen.Logs) },
|
||||
onNavigateToCrashLogs = { pushScreen(Screen.CrashLogs) },
|
||||
onNavigateToBiometric = { pushScreen(Screen.Biometric) },
|
||||
viewModel = profileViewModel,
|
||||
avatarRepository = avatarRepository,
|
||||
@@ -770,13 +820,13 @@ fun MainScreen(
|
||||
// Other screens with swipe back
|
||||
SwipeBackContainer(
|
||||
isVisible = isBackupVisible,
|
||||
onBack = { navStack = navStack.filterNot { it is Screen.Backup } + Screen.Safety },
|
||||
onBack = { navStack = navStack.filterNot { it is Screen.Backup } },
|
||||
isDarkTheme = isDarkTheme
|
||||
) {
|
||||
BackupScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onBack = {
|
||||
navStack = navStack.filterNot { it is Screen.Backup } + Screen.Safety
|
||||
navStack = navStack.filterNot { it is Screen.Backup }
|
||||
},
|
||||
onVerifyPassword = { password ->
|
||||
// Verify password by trying to decrypt the private key
|
||||
@@ -824,11 +874,9 @@ fun MainScreen(
|
||||
accountPrivateKey = accountPrivateKey,
|
||||
onBack = { navStack = navStack.filterNot { it is Screen.Safety } },
|
||||
onBackupClick = {
|
||||
navStack = navStack.filterNot { it is Screen.Safety } + Screen.Backup
|
||||
navStack = navStack + Screen.Backup
|
||||
},
|
||||
onDeleteAccount = {
|
||||
// TODO: Implement account deletion
|
||||
}
|
||||
onDeleteAccount = onDeleteAccount
|
||||
)
|
||||
}
|
||||
|
||||
@@ -893,8 +941,13 @@ fun MainScreen(
|
||||
currentUserName = accountName,
|
||||
onBack = { popChatAndChildren() },
|
||||
onUserProfileClick = { user ->
|
||||
// Открываем профиль другого пользователя
|
||||
pushScreen(Screen.OtherProfile(user))
|
||||
if (user.publicKey == accountPublicKey) {
|
||||
// Свой профиль — открываем My Profile
|
||||
pushScreen(Screen.Profile)
|
||||
} else {
|
||||
// Открываем профиль другого пользователя
|
||||
pushScreen(Screen.OtherProfile(user))
|
||||
}
|
||||
},
|
||||
onNavigateToChat = { forwardUser ->
|
||||
// 📨 Forward: переход в выбранный чат с полными данными
|
||||
@@ -926,6 +979,9 @@ fun MainScreen(
|
||||
navStack =
|
||||
navStack.filterNot { it is Screen.Search } +
|
||||
Screen.ChatDetail(selectedSearchUser)
|
||||
},
|
||||
onNavigateToCrashLogs = {
|
||||
navStack = navStack.filterNot { it is Screen.Search } + Screen.CrashLogs
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -118,6 +118,32 @@ class AccountManager(private val context: Context) {
|
||||
saveAccount(updatedAccount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete account completely - remove from accounts list and clear login state
|
||||
*/
|
||||
suspend fun deleteAccount(publicKey: String) {
|
||||
context.accountDataStore.edit { preferences ->
|
||||
// Remove from accounts list
|
||||
val existingJson = preferences[ACCOUNTS_JSON]
|
||||
if (existingJson != null) {
|
||||
val accounts = parseAccounts(existingJson).toMutableList()
|
||||
accounts.removeAll { it.publicKey == publicKey }
|
||||
preferences[ACCOUNTS_JSON] = serializeAccounts(accounts)
|
||||
}
|
||||
// Clear current login if this was the active account
|
||||
val currentKey = preferences[CURRENT_PUBLIC_KEY]
|
||||
if (currentKey == publicKey) {
|
||||
preferences[IS_LOGGED_IN] = false
|
||||
preferences.remove(CURRENT_PUBLIC_KEY)
|
||||
}
|
||||
}
|
||||
// Clear SharedPreferences if this was the last logged account
|
||||
val lastLogged = sharedPrefs.getString(KEY_LAST_LOGGED, null)
|
||||
if (lastLogged == publicKey) {
|
||||
sharedPrefs.edit().remove(KEY_LAST_LOGGED).commit()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearAll() {
|
||||
context.accountDataStore.edit { it.clear() }
|
||||
}
|
||||
|
||||
@@ -64,4 +64,8 @@ interface BlacklistDao {
|
||||
*/
|
||||
@Query("SELECT public_key FROM blacklist WHERE account = :account")
|
||||
suspend fun getBlockedPublicKeys(account: String): List<String>
|
||||
|
||||
/** Удалить все записи blacklist для аккаунта */
|
||||
@Query("DELETE FROM blacklist WHERE account = :account")
|
||||
suspend fun deleteAllByAccount(account: String)
|
||||
}
|
||||
|
||||
@@ -277,6 +277,10 @@ interface MessageDao {
|
||||
)
|
||||
suspend fun deleteMessagesBetweenUsers(account: String, user1: String, user2: String): Int
|
||||
|
||||
/** Удалить все сообщения аккаунта */
|
||||
@Query("DELETE FROM messages WHERE account = :account")
|
||||
suspend fun deleteAllByAccount(account: String): Int
|
||||
|
||||
/** Количество непрочитанных сообщений в диалоге */
|
||||
@Query(
|
||||
"""
|
||||
@@ -492,6 +496,10 @@ interface DialogDao {
|
||||
@Query("DELETE FROM dialogs WHERE account = :account AND opponent_key = :opponentKey")
|
||||
suspend fun deleteDialog(account: String, opponentKey: String)
|
||||
|
||||
/** Удалить все диалоги аккаунта */
|
||||
@Query("DELETE FROM dialogs WHERE account = :account")
|
||||
suspend fun deleteAllByAccount(account: String)
|
||||
|
||||
/** Обновить информацию о собеседнике */
|
||||
@Query(
|
||||
"""
|
||||
|
||||
@@ -2267,17 +2267,7 @@ fun ChatDetailScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// 🐛 Debug Logs BottomSheet
|
||||
if (showDebugLogs) {
|
||||
DebugLogsBottomSheet(
|
||||
logs = debugLogs,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onDismiss = { showDebugLogs = false },
|
||||
onClearLogs = { ProtocolManager.clearLogs() }
|
||||
)
|
||||
}
|
||||
|
||||
// 📨 Forward Chat Picker BottomSheet
|
||||
// Forward Chat Picker BottomSheet
|
||||
if (showForwardPicker) {
|
||||
ForwardChatPickerBottomSheet(
|
||||
dialogs = dialogsList,
|
||||
|
||||
@@ -46,6 +46,7 @@ import com.rosetta.messenger.ui.components.AvatarImage
|
||||
import com.rosetta.messenger.ui.components.BlurredAvatarBackground
|
||||
import com.rosetta.messenger.ui.components.VerifiedBadge
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlueDark
|
||||
import com.rosetta.messenger.ui.settings.BackgroundBlurPresets
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
@@ -237,9 +238,6 @@ fun ChatsListScreen(
|
||||
val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black }
|
||||
val secondaryTextColor =
|
||||
remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E8E) else Color(0xFF8E8E93) }
|
||||
val drawerGrabZonePx = with(androidx.compose.ui.platform.LocalDensity.current) { 88.dp.toPx() }
|
||||
val drawerOpenDistancePx = with(androidx.compose.ui.platform.LocalDensity.current) { 20.dp.toPx() }
|
||||
val drawerOpenVelocityThresholdPx = with(androidx.compose.ui.platform.LocalDensity.current) { 110.dp.toPx() }
|
||||
|
||||
// Protocol connection state
|
||||
val protocolState by ProtocolManager.state.collectAsState()
|
||||
@@ -277,7 +275,7 @@ fun ChatsListScreen(
|
||||
var visible by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
// Confirmation dialogs state
|
||||
var dialogToDelete by remember { mutableStateOf<DialogUiModel?>(null) }
|
||||
var dialogsToDelete by remember { mutableStateOf<List<DialogUiModel>>(emptyList()) }
|
||||
var dialogToBlock by remember { mutableStateOf<DialogUiModel?>(null) }
|
||||
var dialogToUnblock by remember { mutableStateOf<DialogUiModel?>(null) }
|
||||
|
||||
@@ -290,12 +288,16 @@ fun ChatsListScreen(
|
||||
val mutedChats by preferencesManager.mutedChatsForAccount(accountPublicKey)
|
||||
.collectAsState(initial = emptySet())
|
||||
|
||||
// Перехватываем системный back gesture - не закрываем приложение
|
||||
val activity = context as? android.app.Activity
|
||||
|
||||
// Всегда перехватываем back чтобы predictive back анимация не ломала UI
|
||||
BackHandler(enabled = true) {
|
||||
if (isSelectionMode) {
|
||||
selectedChatKeys = emptySet()
|
||||
} else if (drawerState.isOpen) {
|
||||
scope.launch { drawerState.close() }
|
||||
} else {
|
||||
activity?.moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,96 +449,10 @@ fun ChatsListScreen(
|
||||
Modifier.fillMaxSize()
|
||||
.background(backgroundColor)
|
||||
.navigationBarsPadding()
|
||||
.pointerInput(drawerState.isOpen, showRequestsScreen) {
|
||||
if (showRequestsScreen) return@pointerInput
|
||||
|
||||
val velocityTracker = VelocityTracker()
|
||||
val relaxedTouchSlop = viewConfiguration.touchSlop * 0.8f
|
||||
|
||||
awaitEachGesture {
|
||||
val down =
|
||||
awaitFirstDown(requireUnconsumed = false)
|
||||
|
||||
if (drawerState.isOpen || down.position.x > drawerGrabZonePx) {
|
||||
return@awaitEachGesture
|
||||
}
|
||||
|
||||
velocityTracker.resetTracking()
|
||||
velocityTracker.addPosition(
|
||||
down.uptimeMillis,
|
||||
down.position
|
||||
)
|
||||
var totalDragX = 0f
|
||||
var totalDragY = 0f
|
||||
var claimed = false
|
||||
|
||||
while (true) {
|
||||
val event = awaitPointerEvent()
|
||||
val change =
|
||||
event.changes.firstOrNull {
|
||||
it.id == down.id
|
||||
}
|
||||
?: break
|
||||
|
||||
if (change.changedToUpIgnoreConsumed()) break
|
||||
|
||||
val delta = change.positionChange()
|
||||
totalDragX += delta.x
|
||||
totalDragY += delta.y
|
||||
velocityTracker.addPosition(
|
||||
change.uptimeMillis,
|
||||
change.position
|
||||
)
|
||||
|
||||
if (!claimed) {
|
||||
val distance =
|
||||
kotlin.math.sqrt(
|
||||
totalDragX *
|
||||
totalDragX +
|
||||
totalDragY *
|
||||
totalDragY
|
||||
)
|
||||
if (distance < relaxedTouchSlop)
|
||||
continue
|
||||
|
||||
val horizontalDominance =
|
||||
kotlin.math.abs(
|
||||
totalDragX
|
||||
) >
|
||||
kotlin.math.abs(
|
||||
totalDragY
|
||||
) * 1.15f
|
||||
if (
|
||||
totalDragX > 0 &&
|
||||
horizontalDominance
|
||||
) {
|
||||
claimed = true
|
||||
change.consume()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
change.consume()
|
||||
}
|
||||
}
|
||||
|
||||
val velocityX = velocityTracker.calculateVelocity().x
|
||||
val shouldOpenDrawer =
|
||||
claimed &&
|
||||
(totalDragX >=
|
||||
drawerOpenDistancePx ||
|
||||
velocityX >
|
||||
drawerOpenVelocityThresholdPx)
|
||||
|
||||
if (shouldOpenDrawer && drawerState.isClosed) {
|
||||
scope.launch { drawerState.open() }
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
gesturesEnabled = !showRequestsScreen, // Disable drawer swipe when requests are open
|
||||
gesturesEnabled = !showRequestsScreen,
|
||||
drawerContent = {
|
||||
ModalDrawerSheet(
|
||||
drawerContainerColor = Color.Transparent,
|
||||
@@ -571,7 +487,8 @@ fun ChatsListScreen(
|
||||
BackgroundBlurPresets
|
||||
.getOverlayColors(
|
||||
backgroundBlurColorId
|
||||
)
|
||||
),
|
||||
isDarkTheme = isDarkTheme
|
||||
)
|
||||
|
||||
// Content поверх фона
|
||||
@@ -634,11 +551,7 @@ fun ChatsListScreen(
|
||||
contentDescription =
|
||||
if (isDarkTheme) "Light Mode"
|
||||
else "Dark Mode",
|
||||
tint =
|
||||
if (isDarkTheme)
|
||||
Color.White.copy(alpha = 0.8f)
|
||||
else
|
||||
Color.Black.copy(alpha = 0.7f),
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
@@ -646,7 +559,7 @@ fun ChatsListScreen(
|
||||
|
||||
Spacer(
|
||||
modifier =
|
||||
Modifier.height(14.dp)
|
||||
Modifier.height(8.dp)
|
||||
)
|
||||
|
||||
// Display name
|
||||
@@ -656,11 +569,7 @@ fun ChatsListScreen(
|
||||
fontSize = 16.sp,
|
||||
fontWeight =
|
||||
FontWeight.SemiBold,
|
||||
color =
|
||||
if (isDarkTheme)
|
||||
Color.White
|
||||
else
|
||||
Color.Black
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
@@ -674,13 +583,7 @@ fun ChatsListScreen(
|
||||
text =
|
||||
"@$accountUsername",
|
||||
fontSize = 13.sp,
|
||||
color =
|
||||
if (isDarkTheme)
|
||||
Color.White
|
||||
.copy(alpha = 0.7f)
|
||||
else
|
||||
Color.Black
|
||||
.copy(alpha = 0.7f)
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -699,7 +602,10 @@ fun ChatsListScreen(
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
val menuIconColor =
|
||||
textColor.copy(alpha = 0.6f)
|
||||
if (isDarkTheme) Color(0xFF7A7F85)
|
||||
else textColor.copy(alpha = 0.6f)
|
||||
|
||||
val accentColor = if (isDarkTheme) PrimaryBlueDark else PrimaryBlue
|
||||
|
||||
// 👤 Profile
|
||||
DrawerMenuItemEnhanced(
|
||||
@@ -826,6 +732,7 @@ fun ChatsListScreen(
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
key(isDarkTheme, showRequestsScreen, isSelectionMode) {
|
||||
@@ -876,9 +783,8 @@ fun ChatsListScreen(
|
||||
// Delete
|
||||
IconButton(onClick = {
|
||||
val allDialogs = topLevelChatsState.dialogs
|
||||
val first = selectedChatKeys.firstOrNull()
|
||||
val dlg = allDialogs.find { it.opponentKey == first }
|
||||
if (dlg != null) dialogToDelete = dlg
|
||||
val selected = allDialogs.filter { selectedChatKeys.contains(it.opponentKey) }
|
||||
if (selected.isNotEmpty()) dialogsToDelete = selected
|
||||
selectedChatKeys = emptySet()
|
||||
}) {
|
||||
Icon(
|
||||
@@ -956,8 +862,8 @@ fun ChatsListScreen(
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4),
|
||||
scrolledContainerColor = if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4),
|
||||
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4),
|
||||
scrolledContainerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4),
|
||||
navigationIconContentColor = Color.White,
|
||||
titleContentColor = Color.White,
|
||||
actionIconContentColor = Color.White
|
||||
@@ -1089,9 +995,9 @@ fun ChatsListScreen(
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor =
|
||||
if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4),
|
||||
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4),
|
||||
scrolledContainerColor =
|
||||
if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4),
|
||||
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4),
|
||||
navigationIconContentColor =
|
||||
Color.White,
|
||||
titleContentColor =
|
||||
@@ -1493,8 +1399,8 @@ fun ChatsListScreen(
|
||||
selectedChatKeys + dialog.opponentKey
|
||||
},
|
||||
onDelete = {
|
||||
dialogToDelete =
|
||||
dialog
|
||||
dialogsToDelete =
|
||||
listOf(dialog)
|
||||
},
|
||||
onBlock = {
|
||||
dialogToBlock =
|
||||
@@ -1539,44 +1445,49 @@ fun ChatsListScreen(
|
||||
// Console button removed
|
||||
}
|
||||
}
|
||||
} // Close content Box
|
||||
} // Close ModalNavigationDrawer
|
||||
|
||||
// 🔥 Confirmation Dialogs
|
||||
|
||||
// Delete Dialog Confirmation
|
||||
dialogToDelete?.let { dialog ->
|
||||
if (dialogsToDelete.isNotEmpty()) {
|
||||
val count = dialogsToDelete.size
|
||||
AlertDialog(
|
||||
onDismissRequest = { dialogToDelete = null },
|
||||
onDismissRequest = { dialogsToDelete = emptyList() },
|
||||
containerColor =
|
||||
if (isDarkTheme) Color(0xFF2C2C2E) else Color.White,
|
||||
title = {
|
||||
Text(
|
||||
"Delete Chat",
|
||||
if (count == 1) "Delete Chat" else "Delete $count Chats",
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = textColor
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
"Are you sure you want to delete this chat? This action cannot be undone.",
|
||||
if (count == 1) "Are you sure you want to delete this chat? This action cannot be undone."
|
||||
else "Are you sure you want to delete $count chats? This action cannot be undone.",
|
||||
color = secondaryTextColor
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val opponentKey = dialog.opponentKey
|
||||
dialogToDelete = null
|
||||
val toDelete = dialogsToDelete.toList()
|
||||
dialogsToDelete = emptyList()
|
||||
scope.launch {
|
||||
chatsViewModel.deleteDialog(
|
||||
opponentKey
|
||||
)
|
||||
toDelete.forEach { dialog ->
|
||||
chatsViewModel.deleteDialog(
|
||||
dialog.opponentKey
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { Text("Delete", color = Color(0xFFFF3B30)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { dialogToDelete = null }) {
|
||||
TextButton(onClick = { dialogsToDelete = emptyList() }) {
|
||||
Text("Cancel", color = PrimaryBlue)
|
||||
}
|
||||
}
|
||||
@@ -1662,6 +1573,7 @@ fun ChatsListScreen(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
} // Close Box
|
||||
}
|
||||
|
||||
@@ -2272,6 +2184,9 @@ fun SwipeableDialogItem(
|
||||
}
|
||||
|
||||
// 2. КОНТЕНТ - поверх кнопок, сдвигается при свайпе
|
||||
// 🔥 rememberUpdatedState чтобы pointerInput всегда вызывал актуальные callbacks
|
||||
val currentOnClick by rememberUpdatedState(onClick)
|
||||
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
@@ -2338,12 +2253,12 @@ fun SwipeableDialogItem(
|
||||
|
||||
when (gestureType) {
|
||||
"tap" -> {
|
||||
onClick()
|
||||
currentOnClick()
|
||||
return@awaitEachGesture
|
||||
}
|
||||
"cancelled" -> return@awaitEachGesture
|
||||
"longpress" -> {
|
||||
onLongClick()
|
||||
currentOnLongClick()
|
||||
// Consume remaining events until finger lifts
|
||||
while (true) {
|
||||
val event = awaitPointerEvent()
|
||||
@@ -3317,7 +3232,7 @@ fun DrawerMenuItemEnhanced(
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = textColor,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
@@ -3348,7 +3263,7 @@ fun DrawerDivider(isDarkTheme: Boolean) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Divider(
|
||||
modifier = Modifier.padding(horizontal = 20.dp),
|
||||
color = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFEEEEEE),
|
||||
color = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFC8C8CC),
|
||||
thickness = 0.5.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
@@ -225,9 +225,9 @@ fun ForwardChatPickerBottomSheet(
|
||||
onClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
selectedChats = if (isSelected) {
|
||||
selectedChats - dialog.opponentKey
|
||||
emptySet()
|
||||
} else {
|
||||
selectedChats + dialog.opponentKey
|
||||
setOf(dialog.opponentKey)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -263,6 +263,7 @@ fun ForwardChatPickerBottomSheet(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PrimaryBlue,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFD1D1D6),
|
||||
disabledContentColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF999999)
|
||||
)
|
||||
@@ -274,10 +275,7 @@ fun ForwardChatPickerBottomSheet(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = if (hasSelection)
|
||||
"Forward to ${selectedChats.size} chat${if (selectedChats.size > 1) "s" else ""}"
|
||||
else
|
||||
"Select a chat",
|
||||
text = if (hasSelection) "Forward" else "Select a chat",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
@@ -54,7 +54,8 @@ fun SearchScreen(
|
||||
isDarkTheme: Boolean,
|
||||
protocolState: ProtocolState,
|
||||
onBackClick: () -> Unit,
|
||||
onUserSelect: (SearchUser) -> Unit
|
||||
onUserSelect: (SearchUser) -> Unit,
|
||||
onNavigateToCrashLogs: () -> Unit = {}
|
||||
) {
|
||||
// Context и View для мгновенного закрытия клавиатуры
|
||||
val context = LocalContext.current
|
||||
@@ -84,6 +85,14 @@ fun SearchScreen(
|
||||
val searchResults by searchViewModel.searchResults.collectAsState()
|
||||
val isSearching by searchViewModel.isSearching.collectAsState()
|
||||
|
||||
// Easter egg: navigate to CrashLogs when typing "rosettadev1"
|
||||
LaunchedEffect(searchQuery) {
|
||||
if (searchQuery.trim().equals("rosettadev1", ignoreCase = true)) {
|
||||
searchViewModel.clearSearchQuery()
|
||||
onNavigateToCrashLogs()
|
||||
}
|
||||
}
|
||||
|
||||
// Always reset query/results when leaving Search screen (back/swipe/navigation).
|
||||
DisposableEffect(Unit) { onDispose { searchViewModel.clearSearchQuery() } }
|
||||
|
||||
|
||||
@@ -1753,23 +1753,7 @@ fun KebabMenu(
|
||||
)
|
||||
}
|
||||
|
||||
// Debug Logs
|
||||
KebabMenuItem(
|
||||
icon = TablerIcons.Bug,
|
||||
text = "Debug Logs",
|
||||
onClick = onLogsClick,
|
||||
tintColor = PrimaryBlue,
|
||||
textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.height(0.5.dp)
|
||||
.background(dividerColor)
|
||||
)
|
||||
|
||||
// Delete chat
|
||||
KebabMenuItem(
|
||||
icon = TablerIcons.Trash,
|
||||
text = "Delete Chat",
|
||||
|
||||
@@ -38,37 +38,63 @@ fun BoxScope.BlurredAvatarBackground(
|
||||
avatarRepository: AvatarRepository?,
|
||||
fallbackColor: Color,
|
||||
blurRadius: Float = 25f,
|
||||
alpha: Float = 0.3f,
|
||||
overlayColors: List<Color>? = null
|
||||
alpha: Float = 0.9f,
|
||||
overlayColors: List<Color>? = null,
|
||||
isDarkTheme: Boolean = true
|
||||
) {
|
||||
// Получаем аватары из репозитория
|
||||
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
||||
// В светлой теме: если дефолтный фон (avatar) — синий как шапка chat list,
|
||||
// если выбран кастомный цвет в Appearance — используем его
|
||||
if (!isDarkTheme) {
|
||||
val lightBgModifier = if (overlayColors != null && overlayColors.isNotEmpty()) {
|
||||
if (overlayColors.size == 1) {
|
||||
Modifier.matchParentSize().background(overlayColors[0])
|
||||
} else {
|
||||
Modifier.matchParentSize().background(
|
||||
Brush.linearGradient(colors = overlayColors)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Modifier.matchParentSize().background(Color(0xFF0D8CF4))
|
||||
}
|
||||
Box(modifier = lightBgModifier)
|
||||
return
|
||||
}
|
||||
|
||||
// Если выбран цвет в Appearance — просто сплошной цвет/градиент, без blur
|
||||
if (overlayColors != null && overlayColors.isNotEmpty()) {
|
||||
val bgModifier = if (overlayColors.size == 1) {
|
||||
Modifier.matchParentSize().background(overlayColors[0])
|
||||
} else {
|
||||
Modifier.matchParentSize().background(
|
||||
Brush.linearGradient(colors = overlayColors)
|
||||
)
|
||||
}
|
||||
Box(modifier = bgModifier)
|
||||
return
|
||||
}
|
||||
|
||||
// Нет фона (avatar default) — blur аватарки
|
||||
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
||||
?: remember { mutableStateOf(emptyList()) }
|
||||
|
||||
// Состояние для bitmap и размытого bitmap
|
||||
|
||||
var originalBitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
|
||||
var blurredBitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
|
||||
|
||||
// Декодируем и размываем аватар
|
||||
|
||||
LaunchedEffect(avatars) {
|
||||
if (avatars.isNotEmpty()) {
|
||||
originalBitmap = withContext(Dispatchers.IO) {
|
||||
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
|
||||
}
|
||||
|
||||
// Размываем bitmap (уменьшаем для производительности, затем применяем blur)
|
||||
originalBitmap?.let { bitmap ->
|
||||
blurredBitmap = withContext(Dispatchers.Default) {
|
||||
// Уменьшаем размер для быстрого blur
|
||||
val scaledBitmap = Bitmap.createScaledBitmap(
|
||||
bitmap,
|
||||
bitmap.width / 4,
|
||||
bitmap.height / 4,
|
||||
true
|
||||
)
|
||||
// Применяем blur несколько раз для более гладкого эффекта
|
||||
var result = scaledBitmap
|
||||
repeat(3) {
|
||||
repeat(2) {
|
||||
result = fastBlur(result, (blurRadius / 4).toInt().coerceAtLeast(1))
|
||||
}
|
||||
result
|
||||
@@ -79,71 +105,22 @@ fun BoxScope.BlurredAvatarBackground(
|
||||
blurredBitmap = null
|
||||
}
|
||||
}
|
||||
|
||||
// Используем matchParentSize() чтобы занимать только размер родителя
|
||||
|
||||
Box(modifier = Modifier.matchParentSize()) {
|
||||
if (blurredBitmap != null) {
|
||||
// Показываем размытое изображение
|
||||
Image(
|
||||
bitmap = blurredBitmap!!.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
this.alpha = alpha
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
// Дополнительный overlay — кастомный или стандартный
|
||||
if (overlayColors != null && overlayColors.isNotEmpty()) {
|
||||
// Кастомный цветной overlay
|
||||
val overlayModifier = if (overlayColors.size == 1) {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(overlayColors[0].copy(alpha = 0.55f))
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
colors = overlayColors.map { it.copy(alpha = 0.55f) }
|
||||
)
|
||||
)
|
||||
}
|
||||
Box(modifier = overlayModifier)
|
||||
} else {
|
||||
// Стандартный overlay для затемнения
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(fallbackColor.copy(alpha = 0.3f))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Fallback: когда нет аватарки
|
||||
if (overlayColors != null && overlayColors.isNotEmpty()) {
|
||||
// Кастомный фон без blur
|
||||
val bgModifier = if (overlayColors.size == 1) {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(overlayColors[0])
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(colors = overlayColors)
|
||||
)
|
||||
}
|
||||
Box(modifier = bgModifier)
|
||||
} else {
|
||||
// Стандартный fallback: цветной фон
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(fallbackColor)
|
||||
)
|
||||
}
|
||||
// Нет фото — цвет аватарки
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(fallbackColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ object BackgroundBlurPresets {
|
||||
|
||||
/** Сплошные цвета */
|
||||
private val solidColors = listOf(
|
||||
BackgroundBlurOption("solid_blue", listOf(Color(0xFF2979FF)), "Blue"),
|
||||
BackgroundBlurOption("solid_blue", listOf(Color(0xFF0D8CF4)), "Blue"),
|
||||
BackgroundBlurOption("solid_green", listOf(Color(0xFF4CAF50)), "Green"),
|
||||
BackgroundBlurOption("solid_orange", listOf(Color(0xFFFF9800)), "Orange"),
|
||||
BackgroundBlurOption("solid_red", listOf(Color(0xFFE53935)), "Red"),
|
||||
|
||||
@@ -63,11 +63,12 @@ fun AppearanceScreen(
|
||||
|
||||
BackHandler { onBack() }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(backgroundColor)
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(backgroundColor)
|
||||
) {
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// CONTENT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -105,7 +106,7 @@ fun AppearanceScreen(
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onToggleTheme) {
|
||||
IconButton(onClick = { onToggleTheme() }) {
|
||||
Icon(
|
||||
imageVector = if (isDarkTheme) TablerIcons.Sun else TablerIcons.Moon,
|
||||
contentDescription = "Toggle theme",
|
||||
@@ -154,6 +155,7 @@ fun AppearanceScreen(
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -439,7 +441,7 @@ private fun ColorCircleItem(
|
||||
|
||||
val borderColor by animateColorAsState(
|
||||
targetValue = if (isSelected) {
|
||||
if (isDarkTheme) Color.White else Color(0xFF222222)
|
||||
Color.White
|
||||
} else {
|
||||
Color.Transparent
|
||||
},
|
||||
|
||||
@@ -226,6 +226,14 @@ fun OtherProfileScreen(
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
// Закрываем клавиатуру при открытии экрана
|
||||
LaunchedEffect(Unit) {
|
||||
val imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE)
|
||||
as android.view.inputmethod.InputMethodManager
|
||||
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
|
||||
// 🗑️ Для удаления диалога
|
||||
val database = remember { RosettaDatabase.getDatabase(context) }
|
||||
val messageDao = remember { database.messageDao() }
|
||||
@@ -1664,7 +1672,7 @@ private fun CollapsingOtherProfileHeader(
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🎨 TEXT COLOR - просто по теме: белый в тёмной, чёрный в светлой
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val textColor = Color.White
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth().height(headerHeight)) {
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -1674,9 +1682,10 @@ private fun CollapsingOtherProfileHeader(
|
||||
publicKey = publicKey,
|
||||
avatarRepository = avatarRepository,
|
||||
fallbackColor = avatarColors.backgroundColor,
|
||||
blurRadius = 25f,
|
||||
alpha = 0.3f,
|
||||
overlayColors = com.rosetta.messenger.ui.settings.BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId)
|
||||
blurRadius = 20f,
|
||||
alpha = 0.9f,
|
||||
overlayColors = com.rosetta.messenger.ui.settings.BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId),
|
||||
isDarkTheme = isDarkTheme
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -1730,7 +1739,7 @@ private fun CollapsingOtherProfileHeader(
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
@@ -1751,7 +1760,7 @@ private fun CollapsingOtherProfileHeader(
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "Profile menu",
|
||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
@@ -1820,12 +1829,7 @@ private fun CollapsingOtherProfileHeader(
|
||||
Text(
|
||||
text = statusText,
|
||||
fontSize = onlineFontSize,
|
||||
color =
|
||||
if (isOnline) {
|
||||
Color(0xFF4CAF50)
|
||||
} else {
|
||||
textColor.copy(alpha = 0.7f)
|
||||
}
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ import com.rosetta.messenger.biometric.BiometricAvailability
|
||||
import com.rosetta.messenger.biometric.BiometricPreferences
|
||||
import com.rosetta.messenger.repository.AvatarRepository
|
||||
import com.rosetta.messenger.ui.components.BlurredAvatarBackground
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlueDark
|
||||
|
||||
import com.rosetta.messenger.ui.components.metaball.ProfileMetaballEffect
|
||||
import com.rosetta.messenger.utils.AvatarFileManager
|
||||
@@ -259,7 +261,6 @@ fun ProfileScreen(
|
||||
onNavigateToAppearance: () -> Unit = {},
|
||||
onNavigateToSafety: () -> Unit = {},
|
||||
onNavigateToLogs: () -> Unit = {},
|
||||
onNavigateToCrashLogs: () -> Unit = {},
|
||||
onNavigateToBiometric: () -> Unit = {},
|
||||
viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
|
||||
avatarRepository: AvatarRepository? = null,
|
||||
@@ -759,6 +760,31 @@ fun ProfileScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
// 🔔 NOTIFICATIONS SECTION
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
TelegramSectionTitle(title = "Notifications", isDarkTheme = isDarkTheme)
|
||||
|
||||
run {
|
||||
val preferencesManager = remember { com.rosetta.messenger.data.PreferencesManager(context) }
|
||||
val notificationsEnabled by preferencesManager.notificationsEnabled.collectAsState(initial = true)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
TelegramToggleItem(
|
||||
icon = TablerIcons.Bell,
|
||||
title = "Push Notifications",
|
||||
isEnabled = notificationsEnabled,
|
||||
onToggle = {
|
||||
scope.launch {
|
||||
preferencesManager.setNotificationsEnabled(!notificationsEnabled)
|
||||
}
|
||||
},
|
||||
isDarkTheme = isDarkTheme
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
// ⚙️ SETTINGS SECTION - Telegram style
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
@@ -785,14 +811,6 @@ fun ProfileScreen(
|
||||
title = "Safety",
|
||||
onClick = onNavigateToSafety,
|
||||
isDarkTheme = isDarkTheme,
|
||||
showDivider = true
|
||||
)
|
||||
|
||||
TelegramSettingsItem(
|
||||
icon = TablerIcons.Bug,
|
||||
title = "Crash Logs",
|
||||
onClick = onNavigateToCrashLogs,
|
||||
isDarkTheme = isDarkTheme,
|
||||
showDivider = biometricAvailable is BiometricAvailability.Available
|
||||
)
|
||||
|
||||
@@ -938,7 +956,7 @@ private fun CollapsingProfileHeader(
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🎨 TEXT COLOR - просто по теме: белый в тёмной, чёрный в светлой
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val textColor = Color.White
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 📐 HEADER HEIGHT - ФИКСИРОВАННАЯ! Не меняется при overscroll
|
||||
@@ -959,7 +977,7 @@ private fun CollapsingProfileHeader(
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 📝 TEXT - внизу header зоны, внутри блока
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
val textDefaultY = expandedHeight - 48.dp // Внизу header блока (ближе к низу)
|
||||
val textDefaultY = expandedHeight - 70.dp // Ближе к аватарке
|
||||
val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT / 2
|
||||
|
||||
// Текст меняет позицию только при collapse, НЕ при overscroll
|
||||
@@ -977,9 +995,10 @@ private fun CollapsingProfileHeader(
|
||||
publicKey = publicKey,
|
||||
avatarRepository = avatarRepository,
|
||||
fallbackColor = avatarColors.backgroundColor,
|
||||
blurRadius = 25f,
|
||||
alpha = 0.3f,
|
||||
overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId)
|
||||
blurRadius = 20f,
|
||||
alpha = 0.9f,
|
||||
overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId),
|
||||
isDarkTheme = isDarkTheme
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -1033,7 +1052,7 @@ private fun CollapsingProfileHeader(
|
||||
Icon(
|
||||
imageVector = TablerIcons.ArrowLeft,
|
||||
contentDescription = "Back",
|
||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
@@ -1055,11 +1074,9 @@ private fun CollapsingProfileHeader(
|
||||
text = "Save",
|
||||
color =
|
||||
if (isSaveEnabled) {
|
||||
if (isDarkTheme) Color.White else Color.Black
|
||||
Color.White
|
||||
} else {
|
||||
(if (isDarkTheme) Color.White else Color.Black).copy(
|
||||
alpha = 0.45f
|
||||
)
|
||||
Color.White.copy(alpha = 0.45f)
|
||||
},
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
@@ -1074,7 +1091,7 @@ private fun CollapsingProfileHeader(
|
||||
Icon(
|
||||
imageVector = TablerIcons.DotsVertical,
|
||||
contentDescription = "Profile menu",
|
||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
@@ -1174,7 +1191,8 @@ private fun FullSizeAvatar(
|
||||
}
|
||||
initialLoadComplete = true
|
||||
} else {
|
||||
// Нет аватарки - помечаем загрузку завершенной
|
||||
// Нет аватарки - сбрасываем bitmap и помечаем загрузку завершенной
|
||||
bitmap = null
|
||||
initialLoadComplete = true
|
||||
}
|
||||
}
|
||||
@@ -1323,8 +1341,8 @@ fun ProfileCard(
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
|
||||
@Composable
|
||||
fun TelegramSectionTitle(title: String, isDarkTheme: Boolean) {
|
||||
val textColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93)
|
||||
fun TelegramSectionTitle(title: String, isDarkTheme: Boolean, color: Color? = null) {
|
||||
val textColor = color ?: if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93)
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
@@ -1483,11 +1501,12 @@ private fun TelegramSettingsItem(
|
||||
onClick: () -> Unit,
|
||||
isDarkTheme: Boolean,
|
||||
showDivider: Boolean = false,
|
||||
subtitle: String? = null
|
||||
subtitle: String? = null,
|
||||
iconTint: Color? = null
|
||||
) {
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
val iconColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
val iconColor = iconTint ?: if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
|
||||
|
||||
Column {
|
||||
@@ -1527,85 +1546,109 @@ private fun TelegramSettingsItem(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TelegramBiometricItem(isEnabled: Boolean, onToggle: () -> Unit, isDarkTheme: Boolean) {
|
||||
private fun TelegramToggleItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
isEnabled: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
isDarkTheme: Boolean,
|
||||
showDivider: Boolean = false
|
||||
) {
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
val iconColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
val primaryBlue = Color(0xFF007AFF)
|
||||
val accentColor = if (isDarkTheme) PrimaryBlueDark else PrimaryBlue
|
||||
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable(onClick = onToggle)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.Fingerprint,
|
||||
contentDescription = null,
|
||||
tint = iconColor,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
|
||||
Text(
|
||||
text = "Biometric Authentication",
|
||||
fontSize = 16.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
// iOS-style animated switch
|
||||
val animatedThumbOffset by
|
||||
animateFloatAsState(
|
||||
targetValue = if (isEnabled) 1f else 0f,
|
||||
animationSpec =
|
||||
spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
),
|
||||
label = "switchThumb"
|
||||
)
|
||||
|
||||
val trackColor by
|
||||
animateColorAsState(
|
||||
targetValue =
|
||||
if (isEnabled) primaryBlue
|
||||
else if (isDarkTheme) Color(0xFF39393D) else Color(0xFFE9E9EA),
|
||||
animationSpec = tween(300),
|
||||
label = "trackColor"
|
||||
)
|
||||
|
||||
Box(
|
||||
Column {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.width(51.dp)
|
||||
.height(31.dp)
|
||||
.clip(RoundedCornerShape(15.5.dp))
|
||||
.background(trackColor)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = { onToggle() }
|
||||
)
|
||||
.padding(2.dp)
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable(onClick = onToggle)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = iconColor,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(text = title, fontSize = 16.sp, color = textColor)
|
||||
if (subtitle != null) {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(text = subtitle, fontSize = 13.sp, color = secondaryTextColor)
|
||||
}
|
||||
}
|
||||
|
||||
// Material 2 / old Telegram style switch
|
||||
val thumbOffset by animateFloatAsState(
|
||||
targetValue = if (isEnabled) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 150),
|
||||
label = "thumb"
|
||||
)
|
||||
val trackColor by animateColorAsState(
|
||||
targetValue = if (isEnabled) accentColor.copy(alpha = 0.5f)
|
||||
else if (isDarkTheme) Color(0xFF39393D) else Color(0xFFBDBDBD),
|
||||
animationSpec = tween(durationMillis = 150),
|
||||
label = "track"
|
||||
)
|
||||
val thumbColor by animateColorAsState(
|
||||
targetValue = if (isEnabled) accentColor
|
||||
else if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFFF1F1F1),
|
||||
animationSpec = tween(durationMillis = 150),
|
||||
label = "thumbColor"
|
||||
)
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.size(27.dp)
|
||||
.align(Alignment.CenterStart)
|
||||
.offset(x = (20.dp * animatedThumbOffset))
|
||||
.shadow(
|
||||
elevation = if (isEnabled) 3.dp else 2.dp,
|
||||
shape = CircleShape,
|
||||
spotColor = Color.Black.copy(alpha = 0.15f)
|
||||
)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White)
|
||||
modifier = Modifier
|
||||
.width(37.dp)
|
||||
.height(20.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(trackColor)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onToggle
|
||||
),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset(x = (17.dp * thumbOffset))
|
||||
.size(20.dp)
|
||||
.shadow(2.dp, CircleShape)
|
||||
.clip(CircleShape)
|
||||
.background(thumbColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showDivider) {
|
||||
Divider(
|
||||
color = dividerColor,
|
||||
thickness = 0.5.dp,
|
||||
modifier = Modifier.padding(start = 60.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TelegramBiometricItem(isEnabled: Boolean, onToggle: () -> Unit, isDarkTheme: Boolean) {
|
||||
TelegramToggleItem(
|
||||
icon = TablerIcons.Fingerprint,
|
||||
title = "Biometric Authentication",
|
||||
isEnabled = isEnabled,
|
||||
onToggle = onToggle,
|
||||
isDarkTheme = isDarkTheme
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TelegramLogoutItem(onClick: () -> Unit, isDarkTheme: Boolean) {
|
||||
val redColor = if (isDarkTheme) Color(0xFFFF5555) else Color(0xFFFF3B30)
|
||||
|
||||
@@ -24,6 +24,8 @@ import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlueDark
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -46,10 +48,11 @@ fun SafetyScreen(
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
|
||||
// Copy states
|
||||
var copiedPublicKey by remember { mutableStateOf(false) }
|
||||
var copiedPrivateKey by remember { mutableStateOf(false) }
|
||||
var showDeleteConfirmation by remember { mutableStateOf(false) }
|
||||
|
||||
// Handle back gesture
|
||||
BackHandler { onBack() }
|
||||
@@ -105,7 +108,7 @@ fun SafetyScreen(
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Keys Section - Telegram style
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
TelegramSectionHeader("Keys", secondaryTextColor)
|
||||
TelegramSectionHeader("Keys", Color(0xFF8E8E93))
|
||||
|
||||
TelegramCopyRow(
|
||||
label = "Public Key",
|
||||
@@ -173,7 +176,7 @@ fun SafetyScreen(
|
||||
|
||||
TelegramActionRow(
|
||||
label = "Delete Account",
|
||||
onClick = onDeleteAccount,
|
||||
onClick = { showDeleteConfirmation = true },
|
||||
textColor = redColor,
|
||||
secondaryTextColor = secondaryTextColor,
|
||||
showDivider = false
|
||||
@@ -187,6 +190,40 @@ fun SafetyScreen(
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Account Confirmation Dialog
|
||||
if (showDeleteConfirmation) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteConfirmation = false },
|
||||
containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White,
|
||||
title = {
|
||||
Text(
|
||||
"Delete Account",
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = textColor
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
"You are attempting to delete your account. Are you sure? This action cannot be undone.",
|
||||
color = secondaryTextColor
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showDeleteConfirmation = false
|
||||
onDeleteAccount()
|
||||
}
|
||||
) { Text("Delete", color = Color(0xFFFF3B30)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteConfirmation = false }) {
|
||||
Text("Cancel", color = if (isDarkTheme) PrimaryBlueDark else PrimaryBlue)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
Reference in New Issue
Block a user