feat: Implement modern popup menu with iOS/Telegram style; enhance user interaction with smooth animations and improved design
This commit is contained in:
@@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.graphics.StrokeJoin
|
import androidx.compose.ui.graphics.StrokeJoin
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
@@ -822,113 +823,41 @@ fun ChatDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Выпадающее меню - чистый дизайн без артефактов
|
// 🔥 Современное выпадающее меню в стиле iOS/Telegram
|
||||||
MaterialTheme(
|
ModernPopupMenu(
|
||||||
colorScheme = MaterialTheme.colorScheme.copy(
|
expanded = showMenu,
|
||||||
surface = inputBackgroundColor
|
onDismissRequest = { showMenu = false },
|
||||||
)
|
isDarkTheme = isDarkTheme
|
||||||
) {
|
) {
|
||||||
DropdownMenu(
|
// Block/Unblock User (не показываем для Saved Messages)
|
||||||
expanded = showMenu,
|
if (!isSavedMessages) {
|
||||||
onDismissRequest = { showMenu = false },
|
ModernMenuItem(
|
||||||
modifier = Modifier
|
icon = if (isBlocked) Icons.Default.CheckCircle else Icons.Default.Block,
|
||||||
.width(220.dp)
|
text = if (isBlocked) "Unblock User" else "Block User",
|
||||||
.background(
|
|
||||||
color = inputBackgroundColor,
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// Delete Chat - красный
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Delete,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = Color(0xFFE53935),
|
|
||||||
modifier = Modifier.size(22.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(14.dp))
|
|
||||||
Text(
|
|
||||||
"Delete Chat",
|
|
||||||
color = Color(0xFFE53935),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
onClick = {
|
||||||
showMenu = false
|
showMenu = false
|
||||||
showDeleteConfirm = true
|
if (isBlocked) {
|
||||||
|
showUnblockConfirm = true
|
||||||
|
} else {
|
||||||
|
showBlockConfirm = true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(horizontal = 8.dp)
|
isDarkTheme = isDarkTheme,
|
||||||
.background(inputBackgroundColor),
|
tintColor = PrimaryBlue
|
||||||
colors = MenuDefaults.itemColors(
|
)
|
||||||
textColor = Color(0xFFE53935)
|
}
|
||||||
)
|
|
||||||
|
// Delete Chat - деструктивное действие
|
||||||
|
ModernMenuItem(
|
||||||
|
icon = Icons.Default.Delete,
|
||||||
|
text = "Delete Chat",
|
||||||
|
onClick = {
|
||||||
|
showMenu = false
|
||||||
|
showDeleteConfirm = true
|
||||||
|
},
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
isDestructive = true
|
||||||
)
|
)
|
||||||
|
|
||||||
// Разделитель
|
|
||||||
if (!isSavedMessages) {
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block/Unblock User - синий (не показываем для Saved Messages)
|
|
||||||
if (!isSavedMessages) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
if (isBlocked) Icons.Default.Check else Icons.Default.Block,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = PrimaryBlue,
|
|
||||||
modifier = Modifier.size(22.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(14.dp))
|
|
||||||
Text(
|
|
||||||
if (isBlocked) "Unblock User" else "Block User",
|
|
||||||
color = PrimaryBlue,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
showMenu = false
|
|
||||||
if (isBlocked) {
|
|
||||||
showUnblockConfirm = true
|
|
||||||
} else {
|
|
||||||
showBlockConfirm = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp)
|
|
||||||
.background(inputBackgroundColor),
|
|
||||||
colors = MenuDefaults.itemColors(
|
|
||||||
textColor = 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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2697,3 +2626,118 @@ private fun SkeletonBubble(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔥 Современное выпадающее меню в стиле iOS/Telegram
|
||||||
|
* С blur эффектом, красивыми тенями и плавными анимациями
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun ModernPopupMenu(
|
||||||
|
expanded: Boolean,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
isDarkTheme: Boolean,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
// Анимация появления
|
||||||
|
val transition = updateTransition(targetState = expanded, label = "menu")
|
||||||
|
|
||||||
|
val scale by transition.animateFloat(
|
||||||
|
transitionSpec = {
|
||||||
|
if (targetState) {
|
||||||
|
spring(dampingRatio = 0.8f, stiffness = 400f)
|
||||||
|
} else {
|
||||||
|
tween(150, easing = FastOutSlowInEasing)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = "scale"
|
||||||
|
) { if (it) 1f else 0.92f }
|
||||||
|
|
||||||
|
val alpha by transition.animateFloat(
|
||||||
|
transitionSpec = {
|
||||||
|
if (targetState) tween(200) else tween(100)
|
||||||
|
},
|
||||||
|
label = "alpha"
|
||||||
|
) { if (it) 1f else 0f }
|
||||||
|
|
||||||
|
// Цвета меню
|
||||||
|
val menuBackgroundColor = if (isDarkTheme) {
|
||||||
|
Color(0xFF2C2C2E) // iOS dark mode menu color
|
||||||
|
} else {
|
||||||
|
Color(0xFFFFFFFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
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)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔥 Современный элемент меню с иконкой
|
||||||
|
* Плавные hover эффекты и красивая типографика
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun ModernMenuItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
isDarkTheme: Boolean,
|
||||||
|
tintColor: Color = if (isDarkTheme) Color.White else Color.Black,
|
||||||
|
isDestructive: Boolean = false
|
||||||
|
) {
|
||||||
|
val actualTintColor = if (isDestructive) Color(0xFFFF3B30) else tintColor // iOS red
|
||||||
|
val textColor = if (isDestructive) {
|
||||||
|
Color(0xFFFF3B30)
|
||||||
|
} else if (isDarkTheme) {
|
||||||
|
Color.White
|
||||||
|
} else {
|
||||||
|
Color.Black
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover/pressed состояние
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null
|
||||||
|
) { onClick() }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = actualTintColor,
|
||||||
|
modifier = Modifier.size(22.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
letterSpacing = (-0.2).sp // iOS стиль
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user