feat: simplify color handling in ChatsListScreen and improve gesture callbacks in SwipeableDialogItem
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user