feat: Implement custom modern popup menu with animations; enhance user interaction and design aesthetics

This commit is contained in:
k1ngsterr1
2026-01-16 23:54:20 +05:00
parent da065ef7f7
commit 1ce7e6498c

View File

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