package chats.presentation.components import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.items import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.geometry.Offset import androidx.compose.ui.res.painterResource import ru.knot.messager.R import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import chats.domain.model.Message import chats.domain.model.MediaType import chats.domain.model.Media import chats.domain.model.MessageStatus import coil.compose.AsyncImage import core.presentation.components.AppVideoPlayer import core.presentation.components.AppAudioPlayer import core.presentation.components.AppAvatar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import chats.presentation.components.LinkPreview import android.content.Intent import android.net.Uri import androidx.compose.ui.platform.LocalContext import androidx.compose.material3.* import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.ui.input.pointer.pointerInput import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter import core.presentation.components.AppMediaLightbox @OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class) @Composable fun MessageBubble( message: Message, isCurrentUser: Boolean, onReactionClick: (String) -> Unit = {}, autoPlay: Boolean = false, initialPlaybackSpeed: Float = 1.0f, onVoiceFinished: (Float) -> Unit = {}, onReply: (Message) -> Unit = {}, onReplyClick: (Message) -> Unit = {}, onSelect: (String) -> Unit = {}, onForward: (Message) -> Unit = {}, onPin: (Message) -> Unit = {}, onEdit: (Message) -> Unit = {}, onDelete: (Message, Boolean) -> Unit = { _, _ -> }, isSelected: Boolean = false, isSelectionMode: Boolean = false, onMediaClick: (chats.domain.model.Media) -> Unit = {} ) { val context = LocalContext.current var showReactionPicker by remember { mutableStateOf(false) } var lightboxMediaIndex by remember { mutableStateOf(null) } val contentColor = Color.White var showContextMenu by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) } val backgroundColor = when { isSelected -> if (isCurrentUser) Color(0xFF3096E5).copy(alpha = 0.5f) else Color(0xFF212121).copy(alpha = 0.5f) showContextMenu -> Color(0xFF007AFF) // Even brighter highlight isCurrentUser -> Color(0xFF3096E5) else -> Color(0xFF212121) } val shape = if (isCurrentUser) { RoundedCornerShape(16.dp, 16.dp, 4.dp, 16.dp) } else { RoundedCornerShape(16.dp, 16.dp, 16.dp, 4.dp) } val isVoiceMessage = message.media.any { it.type == "voice" } || (message.mediaType == MediaType.AUDIO && (message.content == null || message.content.isEmpty())) Row( modifier = Modifier .fillMaxWidth() .pointerInput(Unit) { // Clicks anywhere in the Row row now work detectTapGestures( onTap = { if (isSelectionMode) { onSelect(message.id) } else { showContextMenu = !showContextMenu } }, onLongPress = { if (!isSelectionMode) { onSelect(message.id) } } ) } .padding(horizontal = 8.dp, vertical = 2.dp), horizontalArrangement = if (isCurrentUser) Arrangement.End else Arrangement.Start, verticalAlignment = Alignment.Bottom ) { var bubbleHeight by remember { mutableIntStateOf(0) } var isNearBottom by remember { mutableStateOf(false) } Box( modifier = Modifier .widthIn(max = 300.dp) .onGloballyPositioned { coords -> bubbleHeight = coords.size.height val screenHeight = coords.parentLayoutCoordinates?.size?.height ?: 2000 val yInWindow = coords.localToWindow(Offset.Zero).y // If bottom of message is in the last 40% of screen, open menu UP isNearBottom = yInWindow > screenHeight * 0.6f }, contentAlignment = if (isCurrentUser) Alignment.CenterEnd else Alignment.CenterStart ) { // Actual bubble Column( modifier = Modifier .clip(shape) .background(backgroundColor) .padding(8.dp) ) { if (showContextMenu) { val density = androidx.compose.ui.platform.LocalDensity.current val menuWidth = 220.dp val menuWidthPx = with(density) { menuWidth.toPx() } Popup( alignment = if (isNearBottom) { if (isCurrentUser) Alignment.BottomStart else Alignment.BottomEnd } else { if (isCurrentUser) Alignment.TopStart else Alignment.TopEnd }, offset = if (isCurrentUser) { // Message is on RIGHT, show menu to its LEFT IntOffset(-menuWidthPx.toInt() - 20, 0) } else { // Message is on LEFT, show menu to its RIGHT IntOffset(menuWidthPx.toInt() + 20, 0) }, onDismissRequest = { showContextMenu = false }, properties = androidx.compose.ui.window.PopupProperties( focusable = true, dismissOnBackPress = true, dismissOnClickOutside = true ) ) { MessageContextMenu( isMyMessage = isCurrentUser, isPinned = message.isPinned, onReactionSelected = { onReactionClick(it) showContextMenu = false }, onAction = { action -> showContextMenu = false when(action) { "reply" -> onReply(message) "select" -> onSelect(message.id) "forward" -> onForward(message) "pin" -> onPin(message) "edit" -> onEdit(message) "copy" -> { val clipboard = context.getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager val clip = android.content.ClipData.newPlainText("message", message.content) clipboard.setPrimaryClip(clip) } "delete" -> { showDeleteDialog = true } } } ) } } // Forwarded Info if (message.isForwarded) { Text( text = "Forwarded from ${message.forwardedFromName ?: "Unknown"}", style = MaterialTheme.typography.labelSmall, color = contentColor.copy(alpha = 0.7f), fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, modifier = Modifier.padding(bottom = 4.dp) ) } // Reply Info message.replyTo?.let { reply -> Row( modifier = Modifier .fillMaxWidth() .padding(bottom = 6.dp) .clip(RoundedCornerShape(4.dp)) .background(contentColor.copy(alpha = 0.1f)) .height(IntrinsicSize.Min) .clickable { onReplyClick(reply) } ) { Box( modifier = Modifier .fillMaxHeight() .width(2.dp) .background(if (isCurrentUser) Color.White else Color(0xFF3096E5)) ) val context = androidx.compose.ui.platform.LocalContext.current val isReplyImage = reply.mediaType == MediaType.IMAGE || reply.mediaType == MediaType.VIDEO || reply.mediaType == MediaType.GIF if (isReplyImage && reply.media.isNotEmpty()) { AsyncImage( model = reply.media.first().url, contentDescription = null, modifier = Modifier .fillMaxHeight() .width(40.dp) .clip(RoundedCornerShape(0.dp, 4.dp, 4.dp, 0.dp)), contentScale = ContentScale.Crop ) } Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp).weight(1f)) { val accentColor = if (isCurrentUser) Color.White else Color(0xFF3096E5) Text( text = reply.senderName, style = MaterialTheme.typography.labelMedium, color = accentColor, maxLines = 1, fontWeight = androidx.compose.ui.text.font.FontWeight.Bold ) val photoText = androidx.compose.ui.res.stringResource(ru.knot.messager.R.string.reply_photo) val videoText = androidx.compose.ui.res.stringResource(ru.knot.messager.R.string.reply_video) val voiceText = androidx.compose.ui.res.stringResource(ru.knot.messager.R.string.voice_message) val audioText = androidx.compose.ui.res.stringResource(ru.knot.messager.R.string.reply_audio) val fileText = androidx.compose.ui.res.stringResource(ru.knot.messager.R.string.reply_file) val gifText = androidx.compose.ui.res.stringResource(ru.knot.messager.R.string.reply_gif) val mediaText = androidx.compose.ui.res.stringResource(ru.knot.messager.R.string.media) val replyText = remember(reply) { if (!reply.content.isNullOrEmpty()) { reply.content } else { when (reply.mediaType) { MediaType.IMAGE -> photoText MediaType.VIDEO -> videoText MediaType.AUDIO -> if (reply.media.any { it.type == "voice" }) voiceText else audioText MediaType.FILE -> fileText MediaType.GIF -> gifText else -> mediaText } } } Text( text = replyText, style = MaterialTheme.typography.bodySmall, color = contentColor.copy(alpha = 0.8f), maxLines = 1, overflow = TextOverflow.Ellipsis ) } } } // GIF Content (Klipy) if (message.mediaType == MediaType.GIF) { AsyncImage( model = message.content, imageLoader = core.utils.CoilUtils.getGifImageLoader(context), contentDescription = null, modifier = Modifier .fillMaxWidth() .heightIn(max = 300.dp) .clip(RoundedCornerShape(8.dp)) .clickable { /* Handle click */ }, contentScale = ContentScale.Crop ) Spacer(modifier = Modifier.height(4.dp)) } // Loading state for temp media messages if (message.id.startsWith("temp_") && message.mediaType != MediaType.TEXT) { Box( modifier = Modifier .fillMaxWidth() .height(200.dp) .clip(RoundedCornerShape(8.dp)) .background(contentColor.copy(alpha = 0.1f)), contentAlignment = Alignment.Center ) { CircularProgressIndicator( modifier = Modifier.size(24.dp), strokeWidth = 2.dp, color = contentColor.copy(alpha = 0.5f) ) } Spacer(modifier = Modifier.height(4.dp)) } // Media Content (Attachments) if (message.media.isNotEmpty() && message.mediaType != MediaType.GIF) { val mediaCount = message.media.size if (mediaCount == 1) { val mediaItem = message.media[0] when { mediaItem.type.startsWith("image") || message.mediaType == MediaType.IMAGE -> { AsyncImage( model = mediaItem.url, imageLoader = core.utils.CoilUtils.getGifImageLoader(context), contentDescription = null, modifier = Modifier .fillMaxWidth() .heightIn(max = 300.dp) .clip(RoundedCornerShape(8.dp)) .clickable { onMediaClick(mediaItem) }, contentScale = ContentScale.Crop ) } message.mediaType == MediaType.VIDEO || mediaItem.type.startsWith("video") -> { Box( modifier = Modifier .fillMaxWidth() .height(240.dp) .clip(RoundedCornerShape(8.dp)) ) { AppVideoPlayer( url = mediaItem.url, modifier = Modifier.fillMaxSize(), useController = false, isMuted = true, autoPlay = true ) // Прозрачный слой поверх видео для клика Box(modifier = Modifier .fillMaxSize() .background(Color.Transparent) .clickable { onMediaClick(mediaItem) } ) Icon( imageVector = Icons.Default.VolumeOff, contentDescription = null, tint = Color.White.copy(alpha = 0.7f), modifier = Modifier.align(Alignment.TopEnd).padding(8.dp).size(20.dp) ) } } mediaItem.type.startsWith("audio") || mediaItem.filename?.endsWith(".mp2", true) == true || mediaItem.filename?.endsWith(".mp3", true) == true || message.mediaType == MediaType.AUDIO -> { AppAudioPlayer( url = mediaItem.url, name = mediaItem.filename, size = mediaItem.size, isVoiceMessage = isVoiceMessage, initialPlaybackSpeed = initialPlaybackSpeed, autoPlay = autoPlay, onFinished = onVoiceFinished, contentColor = contentColor ) } else -> { FileItem(media = mediaItem, isCurrentUser = isCurrentUser, contentColor = contentColor) } } } else { // Multi-media grid PhotoGrid( mediaList = message.media, isCurrentUser = isCurrentUser, onMediaClick = { index -> onMediaClick(message.media[index]) }, modifier = Modifier.fillMaxWidth().height(300.dp).clip(RoundedCornerShape(12.dp)) ) } Spacer(modifier = Modifier.height(4.dp)) } // Text Content if (!message.content.isNullOrBlank() && !isVoiceMessage && message.mediaType != MediaType.GIF) { Column { val annotatedString = remember(message.content) { val text = message.content ?: "" val links = core.utils.LinkParser.findLinks(text) androidx.compose.ui.text.buildAnnotatedString { append(text) links.forEach { link -> val startIndex = text.indexOf(link) if (startIndex >= 0) { val endIndex = startIndex + link.length addStyle( style = androidx.compose.ui.text.SpanStyle( color = Color(0xFF3096E5), textDecoration = androidx.compose.ui.text.style.TextDecoration.Underline ), start = startIndex, end = endIndex ) addStringAnnotation( tag = "URL", annotation = link, start = startIndex, end = endIndex ) } } } } val context = androidx.compose.ui.platform.LocalContext.current androidx.compose.foundation.text.ClickableText( text = annotatedString, style = MaterialTheme.typography.bodyMedium.copy(color = contentColor), onClick = { offset -> annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset) .firstOrNull()?.let { annotation -> try { val intent = android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(annotation.item)) context.startActivity(intent) } catch (e: Exception) {} } } ) val links = remember(message.content) { core.utils.LinkParser.findLinks(message.content) } if (links.isNotEmpty()) { Spacer(modifier = Modifier.height(8.dp)) LinkPreview( url = links[0], contentColor = contentColor ) } } } // Time and Status Row( modifier = Modifier.align(Alignment.End), verticalAlignment = Alignment.CenterVertically ) { if (message.reactions.isNotEmpty()) { MessageReactions( reactions = message.reactions, onReactionClick = onReactionClick, modifier = Modifier.padding(end = 4.dp) ) } val formattedTime = remember(message.createdAt) { try { val instant = Instant.parse(message.createdAt) val zonedDateTime = instant.atZone(ZoneId.systemDefault()) zonedDateTime.format(DateTimeFormatter.ofPattern("HH:mm")) } catch (e: Exception) { message.createdAt.substringAfter('T').take(5) } } Text( text = formattedTime, style = MaterialTheme.typography.labelSmall, color = contentColor.copy(alpha = 0.6f), fontSize = 11.sp ) if (message.isPinned) { Icon( imageVector = Icons.Default.PushPin, contentDescription = "Pinned", modifier = Modifier.size(12.dp).padding(start = 2.dp).graphicsLayer { rotationZ = 45f }, tint = contentColor.copy(alpha = 0.6f) ) } if (isCurrentUser) { Spacer(modifier = Modifier.width(2.dp)) // Отображение статуса отправки сообщения when (message.status) { MessageStatus.PENDING -> { // Часы - ожидает отправки Icon( imageVector = Icons.Default.Schedule, contentDescription = "Pending", modifier = Modifier.size(12.dp), tint = contentColor.copy(alpha = 0.6f) ) } MessageStatus.SENDING -> { // Круговой индикатор загрузки CircularProgressIndicator( modifier = Modifier.size(10.dp), strokeWidth = 1.5.dp, color = contentColor.copy(alpha = 0.6f) ) } MessageStatus.FAILED -> { // Красный крестик - ошибка отправки Icon( imageVector = Icons.Default.Error, contentDescription = "Failed", modifier = Modifier.size(12.dp), tint = Color(0xFFFF5252) ) } else -> { // Одна галочка для SENT, две для DELIVERED/READ Icon( imageVector = if (message.status == MessageStatus.READ) Icons.Default.DoneAll else Icons.Default.Done, contentDescription = null, modifier = Modifier.size(12.dp), tint = if (message.status == MessageStatus.READ) Color(0xFF4CAF50) else contentColor.copy(alpha = 0.6f) ) } } } } } // End of bubble Column if (isSelected) { Icon( imageVector = Icons.Default.CheckCircle, contentDescription = null, tint = Color.White, modifier = Modifier .align(Alignment.Center) .size(32.dp) .background(Color(0xFF3096E5), CircleShape) .padding(2.dp) ) } } // End of Box } // End of Row lightboxMediaIndex?.let { index -> AppMediaLightbox( mediaList = message.media, initialIndex = index, onClose = { lightboxMediaIndex = null } ) } if (showDeleteDialog) { var deleteForEveryone by remember { mutableStateOf(false) } AlertDialog( onDismissRequest = { showDeleteDialog = false }, title = { Text("Удалить сообщение?") }, text = { Column { Text("Вы уверены, что хотите удалить это сообщение?") if (isCurrentUser) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 8.dp).clickable { deleteForEveryone = !deleteForEveryone } ) { Checkbox( checked = deleteForEveryone, onCheckedChange = { deleteForEveryone = it } ) Text("Удалить у всех") } } } }, confirmButton = { TextButton( onClick = { onDelete(message, deleteForEveryone) showDeleteDialog = false }, colors = ButtonDefaults.textButtonColors(contentColor = Color(0xFFE53935)) ) { Text("Удалить") } }, dismissButton = { TextButton(onClick = { showDeleteDialog = false }) { Text("Отмена") } } ) } } @Composable fun MessageReactions( reactions: Map, onReactionClick: (String) -> Unit, modifier: Modifier = Modifier ) { Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { reactions.forEach { (emoji, count) -> Box( modifier = Modifier .clip(CircleShape) .background(Color.White.copy(alpha = 0.2f)) .clickable { onReactionClick(emoji) } .padding(horizontal = 6.dp, vertical = 2.dp) ) { Row(verticalAlignment = Alignment.CenterVertically) { Text(text = emoji, fontSize = 12.sp) if (count > 1) { Spacer(modifier = Modifier.width(2.dp)) Text(text = count.toString(), fontSize = 10.sp, color = Color.White) } } } } } } @Composable fun FileItem(media: Media, isCurrentUser: Boolean, contentColor: Color) { Row( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(8.dp)) .background(contentColor.copy(alpha = 0.1f)) .padding(8.dp), verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .size(40.dp) .clip(RoundedCornerShape(8.dp)) .background(if (isCurrentUser) Color.White.copy(alpha = 0.2f) else Color(0xFF3390EC).copy(alpha = 0.2f)), contentAlignment = Alignment.Center ) { Icon(Icons.Default.Description, contentDescription = null, tint = contentColor) } Column( modifier = Modifier.weight(1f).padding(horizontal = 8.dp) ) { val fileName = media.filename ?: "File" Text( text = fileName, style = MaterialTheme.typography.bodySmall, color = contentColor, maxLines = 1, overflow = TextOverflow.Ellipsis ) val sizeStr = media.size?.let { if (it > 1024 * 1024) "${it / (1024 * 1024)} MB" else "${it / 1024} KB" } ?: "File" Text( text = sizeStr, style = MaterialTheme.typography.labelSmall, color = contentColor.copy(alpha = 0.6f) ) } Icon(Icons.Default.Download, contentDescription = null, tint = contentColor, modifier = Modifier.size(20.dp)) } } @Composable fun PhotoGrid(mediaList: List, isCurrentUser: Boolean, onMediaClick: (Int) -> Unit, modifier: Modifier = Modifier) { if (mediaList.isEmpty()) return val columns = when { mediaList.size == 1 -> 1 mediaList.size <= 4 -> 2 else -> 3 } Column(modifier = modifier) { val rows = (mediaList.size + columns - 1) / columns for (i in 0 until rows) { Row(modifier = Modifier.weight(1f).fillMaxWidth()) { for (j in 0 until columns) { val index = i * columns + j if (index < mediaList.size) { GridItem( media = mediaList[index], modifier = Modifier .weight(1f) .fillMaxHeight() .padding(1.dp) .clickable { onMediaClick(index) } ) } else { Spacer(modifier = Modifier.weight(1f)) } } } } } } @Composable fun GridItem(media: Media, modifier: Modifier = Modifier) { val context = LocalContext.current val isImage = media.type.startsWith("image") val isVideo = media.type.startsWith("video") Box(modifier = modifier.background(Color.White.copy(alpha = 0.1f))) { if (isImage || isVideo) { AsyncImage( model = media.url, contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) if (isVideo) { Icon( imageVector = Icons.Default.PlayCircle, contentDescription = null, tint = Color.White.copy(alpha = 0.8f), modifier = Modifier.align(Alignment.Center).size(32.dp) ) } } else { Column( modifier = Modifier.fillMaxSize().padding(4.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { Icon( imageVector = Icons.Default.InsertDriveFile, contentDescription = null, tint = Color.White.copy(alpha = 0.7f), modifier = Modifier.size(32.dp) ) Text( text = media.filename ?: "File", style = MaterialTheme.typography.labelSmall, color = Color.White.copy(alpha = 0.9f), maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = androidx.compose.ui.text.style.TextAlign.Center ) Text( text = media.url.substringAfterLast(".").uppercase(), style = MaterialTheme.typography.labelSmall, color = Color.White.copy(alpha = 0.5f), fontSize = 8.sp ) } } } } @Composable fun MessageContextMenu( isMyMessage: Boolean, isPinned: Boolean, onReactionSelected: (String) -> Unit, onAction: (String) -> Unit ) { var isVisible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { isVisible = true } androidx.compose.animation.AnimatedVisibility( visible = isVisible, enter = androidx.compose.animation.fadeIn(animationSpec = androidx.compose.animation.core.tween(200)) + androidx.compose.animation.scaleIn( initialScale = 0.5f, animationSpec = androidx.compose.animation.core.spring( dampingRatio = 0.6f, stiffness = 400f ), transformOrigin = androidx.compose.ui.graphics.TransformOrigin( if (isVisible) 0.5f else 0.5f, 0f ) ), exit = androidx.compose.animation.fadeOut() + androidx.compose.animation.scaleOut() ) { androidx.compose.material3.Surface( modifier = Modifier .width(280.dp) // Even wider for all reactions .clip(RoundedCornerShape(20.dp)), color = Color(0xFF1E1E1E), shadowElevation = 15.dp, border = androidx.compose.foundation.BorderStroke(0.5.dp, Color.White.copy(alpha = 0.15f)) ) { Column { // Extended Reactions scrollable bar androidx.compose.foundation.lazy.LazyRow( modifier = Modifier .fillMaxWidth() .padding(horizontal = 4.dp, vertical = 10.dp), horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = PaddingValues(horizontal = 12.dp) ) { val telegramReactions = listOf( "👍", "❤️", "😂", "🔥", "💯", "👏", "🤩", "🤔", "🤯", "😱", "🤬", "👎", "💩", "🤡", "🥳", "😇", "🌚", "🙏", "👌", "🕊️", "🐳", "🌭", "🦄", "🍌", "💊", "🍓", "🍾", "💋", "🖕", "👀", "👻", "🤝", "🤣", "⚡", "✨", "🎈", "🎉", "🧊", "🆒", "🤨", "😐", "🤷‍♂️", "💅", "🏆", "👾", "🎯", "🎲", "🍷", "🤮", "🥱", "😴", "🤐", "🥴" ) items(telegramReactions) { emoji -> Text( text = emoji, fontSize = 28.sp, modifier = Modifier .clip(CircleShape) .clickable { onReactionSelected(emoji) } .padding(4.dp) ) } } Divider(color = Color.White.copy(alpha = 0.1f)) ContextMenuItem(Icons.Default.Reply, "Ответить", onClick = { onAction("reply") }) ContextMenuItem(Icons.Default.CheckCircle, "Выбрать", onClick = { onAction("select") }) ContextMenuItem(Icons.Default.Forward, "Переслать", onClick = { onAction("forward") }) ContextMenuItem( icon = Icons.Default.PushPin, text = if (isPinned) "Открепить" else "Закрепить", onClick = { onAction("pin") } ) ContextMenuItem(Icons.Default.ContentCopy, "Копировать", onClick = { onAction("copy") }) if (isMyMessage) { ContextMenuItem(Icons.Default.Edit, "Редактировать", onClick = { onAction("edit") }) } Divider(color = Color.White.copy(alpha = 0.1f)) ContextMenuItem( Icons.Default.Delete, "Удалить", textColor = Color(0xFFE53935), onClick = { onAction("delete") } ) } } } } @Composable fun ContextMenuItem( icon: ImageVector, text: String, textColor: Color = Color.White, onClick: () -> Unit ) { Row( modifier = Modifier .fillMaxWidth() .clickable(onClick = onClick) .padding(horizontal = 16.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( imageVector = icon, contentDescription = null, tint = textColor.copy(alpha = 0.7f), modifier = Modifier.size(20.dp) ) Spacer(modifier = Modifier.width(16.dp)) Text( text = text, color = textColor, style = MaterialTheme.typography.bodyMedium ) } }