Files
forkmessager/client-mobile/chats/presentation/components/MessageBubble.kt
2026-04-14 01:41:51 +03:00

205 lines
7.5 KiB
Kotlin

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 chats.presentation.components.LinkPreview
import android.content.Intent
import android.net.Uri
import androidx.compose.ui.platform.LocalContext
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
@Composable
fun MessageBubble(
message: Message,
isCurrentUser: Boolean,
onReactionClick: (String) -> Unit = {}
) {
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(12.dp, 12.dp, 4.dp, 12.dp)
} else {
RoundedCornerShape(12.dp, 12.dp, 12.dp, 4.dp)
}
val isVoiceMessage = message.mediaType == MediaType.AUDIO &&
(message.content == null || message.content.isEmpty())
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp),
contentAlignment = alignment
) {
Column(
modifier = Modifier
.clip(shape)
.background(backgroundColor)
.combinedClickable(
onClick = { /* Handle normal click */ },
onLongClick = { showReactionPicker = true }
)
.padding(8.dp)
.widthIn(max = 300.dp)
) {
if (showReactionPicker) {
Popup(
alignment = Alignment.TopCenter,
offset = IntOffset(0, -100),
onDismissRequest = { showReactionPicker = false }
) {
ReactionPicker(onReactionSelected = {
onReactionClick(it)
showReactionPicker = false
})
}
}
// Sender Name (only for others)
if (!isCurrentUser) {
Text(
text = message.senderName,
style = MaterialTheme.typography.labelSmall,
color = contentColor.copy(alpha = 0.7f),
modifier = Modifier.padding(bottom = 2.dp)
)
}
// Reply Info
message.replyTo?.let { reply ->
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 4.dp)
.clip(RoundedCornerShape(4.dp))
.background(contentColor.copy(alpha = 0.1f))
.padding(8.dp)
) {
Column {
Text(
text = reply.senderName,
style = MaterialTheme.typography.labelSmall,
color = contentColor,
maxLines = 1
)
Text(
text = reply.content ?: "[Media]",
style = MaterialTheme.typography.bodySmall,
color = contentColor.copy(alpha = 0.7f),
maxLines = 1
)
}
}
}
// Media Content
if (message.mediaUrl != null) {
when (message.mediaType) {
MediaType.IMAGE -> {
AsyncImage(
model = message.mediaUrl,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 200.dp)
.clip(RoundedCornerShape(12.dp)),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.height(4.dp))
}
MediaType.VIDEO -> {
AppVideoPlayer(
url = message.mediaUrl,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(12.dp)),
useController = true,
autoPlay = false
)
Spacer(modifier = Modifier.height(4.dp))
}
MediaType.AUDIO -> {
AppAudioPlayer(
url = message.mediaUrl,
isVoiceMessage = isVoiceMessage,
contentColor = contentColor
)
Spacer(modifier = Modifier.height(4.dp))
}
else -> {}
}
}
// Text Content (Story reply or simple text)
if (message.mediaType == MediaType.STORY_REPLY) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 4.dp)
.clip(RoundedCornerShape(8.dp))
.background(contentColor.copy(alpha = 0.1f))
.padding(8.dp)
) {
Text("Story Reply: ${message.content}", color = contentColor, style = MaterialTheme.typography.bodyMedium)
}
} else 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
) {
Text(
text = message.createdAt.takeLast(5),
style = MaterialTheme.typography.labelSmall,
color = contentColor.copy(alpha = 0.6f)
)
}
// Reactions
if (message.reactions.isNotEmpty()) {
MessageReactions(
reactions = message.reactions,
onReactionClick = onReactionClick
)
}
}
}
}