205 lines
7.5 KiB
Kotlin
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
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|