feat: simplify color handling in ChatsListScreen and improve gesture callbacks in SwipeableDialogItem

This commit is contained in:
2026-02-12 20:09:53 +05:00
parent ea537ccce1
commit e208ce050a
16 changed files with 419 additions and 365 deletions

View File

@@ -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,

View File

@@ -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))

View File

@@ -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
)

View File

@@ -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() } }

View File

@@ -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",