package chats.presentation.components import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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.layout.ContentScale 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 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.Description import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.DoneAll import chats.presentation.components.LinkPreview import android.content.Intent import android.net.Uri import androidx.compose.ui.platform.LocalContext import androidx.compose.material3.Icon import androidx.compose.ui.text.style.TextOverflow import androidx.compose.foundation.shape.CircleShape @OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class) @Composable fun MessageBubble( message: Message, isCurrentUser: Boolean, senderAvatar: String? = null, onReactionClick: (String) -> Unit = {} ) { val context = LocalContext.current var showReactionPicker by remember { mutableStateOf(false) } val backgroundColor = if (isCurrentUser) { Color(0xFF3390EC) // Telegram Blue } else { Color(0xFF2B2B2B) // Dark Grey } val contentColor = Color.White val alignment = if (isCurrentUser) Alignment.CenterEnd else Alignment.CenterStart 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.mediaType == MediaType.AUDIO && (message.content == null || message.content.isEmpty()) Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 2.dp), horizontalArrangement = if (isCurrentUser) Arrangement.End else Arrangement.Start, verticalAlignment = Alignment.Bottom ) { if (!isCurrentUser) { AppAvatar( url = senderAvatar, name = message.senderName, size = 32.dp, modifier = Modifier.padding(end = 8.dp, bottom = 4.dp) ) } Column( modifier = Modifier .widthIn(max = 300.dp) .clip(shape) .background(backgroundColor) .combinedClickable( onClick = { /* Handle normal click */ }, onLongClick = { showReactionPicker = true } ) .padding(8.dp) ) { if (showReactionPicker) { Popup( alignment = Alignment.TopCenter, offset = IntOffset(0, -100), onDismissRequest = { showReactionPicker = false } ) { ReactionPicker(onReactionSelected = { onReactionClick(it) showReactionPicker = false }) } } // 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) ) { Box( modifier = Modifier .fillMaxHeight() .width(2.dp) .background(if (isCurrentUser) Color.White else Color(0xFF3390EC)) ) Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { Text( text = reply.senderName, style = MaterialTheme.typography.labelMedium, color = if (isCurrentUser) Color.White else Color(0xFF3390EC), maxLines = 1, fontWeight = androidx.compose.ui.text.font.FontWeight.Bold ) Text( text = reply.content ?: "[Media]", style = MaterialTheme.typography.bodySmall, color = contentColor.copy(alpha = 0.8f), maxLines = 1 ) } } } // Media Content if (message.media.isNotEmpty()) { val mediaCount = message.media.size if (mediaCount == 1) { val mediaUrl = message.media[0] when (message.mediaType) { MediaType.IMAGE -> { AsyncImage( model = mediaUrl, contentDescription = null, modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .clickable { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(mediaUrl)) context.startActivity(intent) }, contentScale = ContentScale.FillWidth ) } MediaType.VIDEO -> { AppVideoPlayer( url = mediaUrl, modifier = Modifier .fillMaxWidth() .height(200.dp) .clip(RoundedCornerShape(12.dp)), useController = true, autoPlay = false ) } MediaType.AUDIO -> { AppAudioPlayer( url = mediaUrl, isVoiceMessage = isVoiceMessage, contentColor = contentColor ) } MediaType.FILE -> { FileItem(mediaUrl = mediaUrl, isCurrentUser = isCurrentUser, contentColor = contentColor) } else -> {} } } else if (mediaCount > 1) { // Photo Grid for multiple images PhotoGrid( urls = message.media, modifier = Modifier .fillMaxWidth() .height(300.dp) .clip(RoundedCornerShape(12.dp)) ) } if (mediaCount > 0) { Spacer(modifier = Modifier.height(4.dp)) } } // Text Content if (!message.content.isNullOrBlank() && !isVoiceMessage) { Text( text = message.content, style = MaterialTheme.typography.bodyMedium, color = 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) ) } Text( text = message.createdAt.takeLast(5), style = MaterialTheme.typography.labelSmall, color = contentColor.copy(alpha = 0.6f), fontSize = 10.sp ) if (isCurrentUser) { Spacer(modifier = Modifier.width(2.dp)) Icon( imageVector = if (message.isRead) Icons.Default.DoneAll else Icons.Default.Done, contentDescription = null, modifier = Modifier.size(12.dp), tint = contentColor.copy(alpha = 0.6f) ) } } } } } @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(mediaUrl: String, 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 = remember(mediaUrl) { val decoded = Uri.decode(mediaUrl.substringAfterLast("/")) if (decoded.length > 30) { decoded.take(15) + "..." + decoded.takeLast(10) } else { decoded } } Text( text = fileName, style = MaterialTheme.typography.bodySmall, color = contentColor, maxLines = 1, overflow = TextOverflow.Ellipsis ) Text( text = if (mediaUrl.endsWith(".mp3", ignoreCase = true) || mediaUrl.contains("audio")) "Audio" else "File", 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(urls: List, modifier: Modifier = Modifier) { val items = urls.take(4) Column(modifier = modifier) { val rows = (items.size + 1) / 2 for (i in 0 until rows) { Row(modifier = Modifier.weight(1f)) { val firstIndex = i * 2 AsyncImage( model = items[firstIndex], contentDescription = null, modifier = Modifier .weight(1f) .fillMaxHeight() .padding(1.dp), contentScale = ContentScale.Crop ) if (firstIndex + 1 < items.size) { AsyncImage( model = items[firstIndex + 1], contentDescription = null, modifier = Modifier .weight(1f) .fillMaxHeight() .padding(1.dp), contentScale = ContentScale.Crop ) } else if (rows > 1) { Spacer(modifier = Modifier.weight(1f)) } } } } }