feat: Implement custom modern popup menu with animations; enhance user interaction and design aesthetics
This commit is contained in:
@@ -11,6 +11,7 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.only
|
||||
@@ -51,6 +52,8 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -804,61 +807,21 @@ fun ChatDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Кнопка меню с выпадающим списком
|
||||
Box {
|
||||
IconButton(
|
||||
onClick = {
|
||||
// 🔥 НЕ закрываем клавиатуру при открытии меню
|
||||
showMenu = true
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.MoreVert,
|
||||
contentDescription = "More",
|
||||
tint = headerIconColor,
|
||||
modifier = Modifier.size(26.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// 🔥 Современное выпадающее меню в стиле iOS/Telegram
|
||||
ModernPopupMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
isDarkTheme = isDarkTheme
|
||||
) {
|
||||
// Block/Unblock User (не показываем для Saved Messages)
|
||||
if (!isSavedMessages) {
|
||||
ModernMenuItem(
|
||||
icon = if (isBlocked) Icons.Default.CheckCircle else Icons.Default.Block,
|
||||
text = if (isBlocked) "Unblock User" else "Block User",
|
||||
onClick = {
|
||||
showMenu = false
|
||||
if (isBlocked) {
|
||||
showUnblockConfirm = true
|
||||
} else {
|
||||
showBlockConfirm = true
|
||||
}
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
tintColor = PrimaryBlue
|
||||
)
|
||||
}
|
||||
|
||||
// Delete Chat - деструктивное действие
|
||||
ModernMenuItem(
|
||||
icon = Icons.Default.Delete,
|
||||
text = "Delete Chat",
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showDeleteConfirm = true
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
isDestructive = true
|
||||
)
|
||||
}
|
||||
// Кнопка меню - открывает bottom sheet
|
||||
IconButton(
|
||||
onClick = {
|
||||
showMenu = true
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.MoreVert,
|
||||
contentDescription = "More",
|
||||
tint = headerIconColor,
|
||||
modifier = Modifier.size(26.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1435,7 +1398,68 @@ fun ChatDetailScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// 📨 Forward Chat Picker BottomSheet
|
||||
// <EFBFBD> Bottom Sheet меню (вместо popup menu)
|
||||
if (showMenu) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showMenu = false },
|
||||
containerColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White,
|
||||
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp),
|
||||
dragHandle = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp)
|
||||
.width(36.dp)
|
||||
.height(4.dp)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.background(if (isDarkTheme) Color.White.copy(alpha = 0.3f) else Color.Black.copy(alpha = 0.2f))
|
||||
)
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 32.dp)
|
||||
) {
|
||||
// Block/Unblock User
|
||||
if (!isSavedMessages) {
|
||||
BottomSheetMenuItem(
|
||||
icon = if (isBlocked) Icons.Default.CheckCircle else Icons.Default.Block,
|
||||
text = if (isBlocked) "Unblock User" else "Block User",
|
||||
onClick = {
|
||||
showMenu = false
|
||||
if (isBlocked) {
|
||||
showUnblockConfirm = true
|
||||
} else {
|
||||
showBlockConfirm = true
|
||||
}
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
tintColor = PrimaryBlue
|
||||
)
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
thickness = 0.5.dp,
|
||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
|
||||
)
|
||||
}
|
||||
|
||||
// Delete Chat
|
||||
BottomSheetMenuItem(
|
||||
icon = Icons.Default.Delete,
|
||||
text = "Delete Chat",
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showDeleteConfirm = true
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
isDestructive = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// <20>📨 Forward Chat Picker BottomSheet
|
||||
if (showForwardPicker) {
|
||||
ForwardChatPickerBottomSheet(
|
||||
dialogs = dialogsList,
|
||||
@@ -2628,8 +2652,9 @@ private fun SkeletonBubble(
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 Современное выпадающее меню в стиле iOS/Telegram
|
||||
* С blur эффектом, красивыми тенями и плавными анимациями
|
||||
* 🔥 Современное выпадающее меню в стиле Telegram
|
||||
* Использует Popup вместо Material DropdownMenu
|
||||
* С красивыми анимациями как в оригинальном Telegram
|
||||
*/
|
||||
@Composable
|
||||
private fun ModernPopupMenu(
|
||||
@@ -2638,64 +2663,75 @@ private fun ModernPopupMenu(
|
||||
isDarkTheme: Boolean,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
// Анимация появления
|
||||
val transition = updateTransition(targetState = expanded, label = "menu")
|
||||
if (!expanded) return
|
||||
|
||||
val scale by transition.animateFloat(
|
||||
transitionSpec = {
|
||||
if (targetState) {
|
||||
spring(dampingRatio = 0.8f, stiffness = 400f)
|
||||
} else {
|
||||
tween(150, easing = FastOutSlowInEasing)
|
||||
}
|
||||
},
|
||||
// Анимация появления в стиле Telegram
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (expanded) 1f else 0.3f,
|
||||
animationSpec = spring(dampingRatio = 0.75f, stiffness = 350f),
|
||||
label = "scale"
|
||||
) { if (it) 1f else 0.92f }
|
||||
)
|
||||
|
||||
val alpha by transition.animateFloat(
|
||||
transitionSpec = {
|
||||
if (targetState) tween(200) else tween(100)
|
||||
},
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = if (expanded) 1f else 0f,
|
||||
animationSpec = tween(150, easing = FastOutSlowInEasing),
|
||||
label = "alpha"
|
||||
) { if (it) 1f else 0f }
|
||||
)
|
||||
|
||||
// Цвета меню
|
||||
// Цвета меню с акцентным оттенком
|
||||
val menuBackgroundColor = if (isDarkTheme) {
|
||||
Color(0xFF2C2C2E) // iOS dark mode menu color
|
||||
Color(0xFF212121) // Telegram dark menu
|
||||
} else {
|
||||
Color(0xFFFFFFFF)
|
||||
Color.White
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
val accentBorderColor = PrimaryBlue.copy(alpha = 0.3f) // Тонкая акцентная обводка
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.TopEnd,
|
||||
offset = IntOffset(-16, 60), // Отступ от кнопки меню
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = Modifier
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
this.alpha = alpha
|
||||
transformOrigin = TransformOrigin(0.85f, 0f)
|
||||
}
|
||||
.width(200.dp)
|
||||
.shadow(
|
||||
elevation = 16.dp,
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
ambientColor = Color.Black.copy(alpha = 0.12f),
|
||||
spotColor = Color.Black.copy(alpha = 0.08f)
|
||||
)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(menuBackgroundColor)
|
||||
properties = PopupProperties(
|
||||
focusable = true,
|
||||
dismissOnBackPress = true,
|
||||
dismissOnClickOutside = true
|
||||
)
|
||||
) {
|
||||
content()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
this.alpha = alpha
|
||||
transformOrigin = TransformOrigin(1f, 0f) // Анимация от правого верхнего угла
|
||||
}
|
||||
.width(220.dp)
|
||||
.shadow(
|
||||
elevation = 8.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
spotColor = PrimaryBlue.copy(alpha = 0.2f),
|
||||
ambientColor = PrimaryBlue.copy(alpha = 0.1f)
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = accentBorderColor,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(menuBackgroundColor)
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 Современный элемент меню с иконкой
|
||||
* Плавные hover эффекты и красивая типографика
|
||||
* 🔥 Элемент меню для Bottom Sheet
|
||||
* Красивый дизайн с большими отступами для удобного нажатия
|
||||
*/
|
||||
@Composable
|
||||
private fun ModernMenuItem(
|
||||
private fun BottomSheetMenuItem(
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
@@ -2703,7 +2739,7 @@ private fun ModernMenuItem(
|
||||
tintColor: Color = if (isDarkTheme) Color.White else Color.Black,
|
||||
isDestructive: Boolean = false
|
||||
) {
|
||||
val actualTintColor = if (isDestructive) Color(0xFFFF3B30) else tintColor // iOS red
|
||||
val actualTintColor = if (isDestructive) Color(0xFFFF3B30) else tintColor
|
||||
val textColor = if (isDestructive) {
|
||||
Color(0xFFFF3B30)
|
||||
} else if (isDarkTheme) {
|
||||
@@ -2712,32 +2748,40 @@ private fun ModernMenuItem(
|
||||
Color.Black
|
||||
}
|
||||
|
||||
// Hover/pressed состояние
|
||||
// Ripple эффект при нажатии
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isPressed = interactionSource.collectIsPressedAsState()
|
||||
|
||||
val backgroundColor = if (isPressed.value) {
|
||||
if (isDarkTheme) Color.White.copy(alpha = 0.08f)
|
||||
else Color.Black.copy(alpha = 0.04f)
|
||||
} else {
|
||||
Color.Transparent
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null
|
||||
) { onClick() }
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
.padding(horizontal = 24.dp, vertical = 20.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = actualTintColor,
|
||||
modifier = Modifier.size(22.dp)
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
Text(
|
||||
text = text,
|
||||
color = textColor,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
letterSpacing = (-0.2).sp // iOS стиль
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user