dm-129
This commit is contained in:
parent
c2e81c8d7b
commit
b41e149095
1 changed files with 95 additions and 110 deletions
|
|
@ -17,14 +17,12 @@ import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Warning
|
import androidx.compose.material.icons.filled.Warning
|
||||||
|
|
@ -70,7 +68,6 @@ import com.example.login_v3.home.Message.ViewModel.Detail.DeleteMessageState
|
||||||
import com.example.login_v3.home.Message.ViewModel.Detail.MessageStatus
|
import com.example.login_v3.home.Message.ViewModel.Detail.MessageStatus
|
||||||
import com.example.login_v3.home.Message.ViewModel.Detail.MessagesUiState
|
import com.example.login_v3.home.Message.ViewModel.Detail.MessagesUiState
|
||||||
import com.example.login_v3.home.Message.ViewModel.Detail.SendMessageState
|
import com.example.login_v3.home.Message.ViewModel.Detail.SendMessageState
|
||||||
import com.example.login_v3.home.Message.ViewModel.UserStatus
|
|
||||||
import com.example.login_v3.navigation.BottomBarViewModel
|
import com.example.login_v3.navigation.BottomBarViewModel
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
|
@ -514,32 +511,22 @@ fun MessageRow(
|
||||||
val contentLowerCase = (message.content ?: "").lowercase()
|
val contentLowerCase = (message.content ?: "").lowercase()
|
||||||
val imageExtensions = listOf(".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif")
|
val imageExtensions = listOf(".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif")
|
||||||
|
|
||||||
// 修正:只要字串包含圖片副檔名,就認定它是舊版圖片路徑(拿掉 startsWith("http") 的限制)
|
|
||||||
val isLegacyImage = imageExtensions.any { contentLowerCase.contains(it) }
|
val isLegacyImage = imageExtensions.any { contentLowerCase.contains(it) }
|
||||||
val isImage = isAttachmentImage || isLegacyImage
|
val isImage = isAttachmentImage || isLegacyImage
|
||||||
|
|
||||||
|
|
||||||
val rawContent = message.content ?: ""
|
val rawContent = message.content ?: ""
|
||||||
|
|
||||||
val finalImageModel = remember(message) {
|
val finalImageModel = remember(message) {
|
||||||
// 1. 取得原始路徑(優先使用 attachment 的 filename,次之用 content)
|
|
||||||
val rawPath = if (isAttachmentImage && !message.attachment?.filename.isNullOrBlank()) {
|
val rawPath = if (isAttachmentImage && !message.attachment?.filename.isNullOrBlank()) {
|
||||||
message.attachment!!.filename
|
message.attachment!!.filename
|
||||||
} else {
|
} else {
|
||||||
message.content ?: ""
|
message.content ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
val baseUrl = "https://tg.technologia-tw.com"
|
val baseUrl = "https://tg.technologia-tw.com"
|
||||||
|
|
||||||
when {
|
when {
|
||||||
rawPath.isBlank() -> ""
|
rawPath.isBlank() -> ""
|
||||||
// 如果後端已經給了完整網址(包含 http 或 https),就直接使用
|
|
||||||
rawPath.startsWith("http") -> rawPath
|
rawPath.startsWith("http") -> rawPath
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
// 💡 關鍵安全處理:先把開頭可能重複的 "/" 或 "uploads/" 乾淨地剝離,避免網址重複拼接
|
|
||||||
val cleanPath = rawPath.removePrefix("/").removePrefix("uploads/")
|
val cleanPath = rawPath.removePrefix("/").removePrefix("uploads/")
|
||||||
|
|
||||||
if (cleanPath.startsWith("attachment/")) {
|
if (cleanPath.startsWith("attachment/")) {
|
||||||
"$baseUrl/uploads/$cleanPath"
|
"$baseUrl/uploads/$cleanPath"
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -549,8 +536,6 @@ fun MessageRow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
val finalBubbleColor = remember(message.status, isHighlight) {
|
val finalBubbleColor = remember(message.status, isHighlight) {
|
||||||
val baseColor = when {
|
val baseColor = when {
|
||||||
isMe && isHighlight -> Color(0xFFB4E197)
|
isMe && isHighlight -> Color(0xFFB4E197)
|
||||||
|
|
@ -558,13 +543,7 @@ fun MessageRow(
|
||||||
!isMe && isHighlight -> Color(0xFFCFD8DC)
|
!isMe && isHighlight -> Color(0xFFCFD8DC)
|
||||||
else -> Color(0xFFECEFF1)
|
else -> Color(0xFFECEFF1)
|
||||||
}
|
}
|
||||||
|
if (message.status == MessageStatus.SENDING) baseColor.copy(alpha = 0.75f) else baseColor
|
||||||
// ⏳ 如果是發送中,直接讓顏色本身帶 alpha(例如 0.75f,防止被背景吃掉)
|
|
||||||
if (message.status == MessageStatus.SENDING) {
|
|
||||||
baseColor.copy(alpha = 0.75f)
|
|
||||||
} else {
|
|
||||||
baseColor // 成功或失敗時維持實色
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -572,12 +551,10 @@ fun MessageRow(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(if (isHighlight) Modifier.background(Color(0x33FF9800), RoundedCornerShape(12.dp)) else Modifier)
|
.then(if (isHighlight) Modifier.background(Color(0x33FF9800), RoundedCornerShape(12.dp)) else Modifier)
|
||||||
) {
|
) {
|
||||||
//message container
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 4.dp, vertical = 2.dp)
|
.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||||
,
|
|
||||||
horizontalArrangement = if (isMe) Arrangement.End else Arrangement.Start
|
horizontalArrangement = if (isMe) Arrangement.End else Arrangement.Start
|
||||||
) {
|
) {
|
||||||
if (!isMe) {
|
if (!isMe) {
|
||||||
|
|
@ -587,114 +564,92 @@ fun MessageRow(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 💡 核心優化:如果是「我發的」且「發送失敗」,我們要把驚嘆號跟氣泡橫向並排
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// 🛑 如果是我發的訊息,且發送失敗(FAILED),就把紅色驚嘆號顯示在氣泡「左邊」
|
|
||||||
if (isMe && message.status == MessageStatus.FAILED) {
|
if (isMe && message.status == MessageStatus.FAILED) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = androidx.compose.material.icons.Icons.Default.Warning, // 需確保有 import
|
imageVector = androidx.compose.material.icons.Icons.Default.Warning,
|
||||||
contentDescription = "發送失敗",
|
contentDescription = "發送失敗",
|
||||||
tint = Color.Red,
|
tint = Color.Red,
|
||||||
modifier = Modifier
|
modifier = Modifier.size(20.dp).clickable { /* 重發邏輯 */ }
|
||||||
.size(20.dp)
|
|
||||||
.clickable {
|
|
||||||
// 💡 加分功能:未來可以在這裡綁定「點擊重發」的 viewModel.sendMessage(...) 邏輯
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = if (isMe) Alignment.End else Alignment.Start
|
horizontalAlignment = if (isMe) Alignment.End else Alignment.Start
|
||||||
) {
|
) {
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.widthIn(max = 280.dp)
|
|
||||||
.clip(
|
|
||||||
RoundedCornerShape(
|
|
||||||
topStart = 12.dp, topEnd = 12.dp,
|
|
||||||
bottomStart = if (isMe) 12.dp else 0.dp,
|
|
||||||
bottomEnd = if (isMe) 0.dp else 12.dp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.combinedClickable(
|
|
||||||
onLongClick = { onReplyClick(message) },
|
|
||||||
onClick = { onRowClick(message) }
|
|
||||||
)
|
|
||||||
.background(color = finalBubbleColor)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
|
|
||||||
// 🌟 核心修復:如果存在被回覆的訊息,在這裡將它渲染出來!
|
// 🌟 核心分流:如果是圖片,不穿氣泡外衣,直接渲染
|
||||||
|
if (isImage) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 240.dp) // 限制圖片最大寬度
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = { onReplyClick(message) },
|
||||||
|
onClick = { onRowClick(message) }
|
||||||
|
),
|
||||||
|
horizontalAlignment = if (isMe) Alignment.End else Alignment.Start
|
||||||
|
) {
|
||||||
|
// 1. 如果有被回覆的訊息,依然顯示在圖片上方(可選,如果不需要也可以移進去文字氣泡)
|
||||||
message.repliedMessage?.let { replied ->
|
message.repliedMessage?.let { replied ->
|
||||||
Row(
|
RepliedMessagePreview(replied, isMe, partnerDisplayName, currentUserId)
|
||||||
modifier = Modifier
|
|
||||||
.padding(bottom = 6.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(4.dp))
|
|
||||||
.background(Color(0x1A000000)) // 給予一層淡淡的黑色半透明背景做為區隔
|
|
||||||
.padding(start = 6.dp) // 留空間給左側邊條
|
|
||||||
) {
|
|
||||||
// 左側的垂直裝飾邊條
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(3.dp)
|
|
||||||
.height(32.dp)
|
|
||||||
.background(if (isMe) Color(0xFF4CAF50) else Color(0xFF78909C)) // 根據是誰發的決定邊條顏色
|
|
||||||
)
|
|
||||||
|
|
||||||
// 被回覆訊息的內容預覽
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = if (replied.senderId == currentUserId) "你" else partnerDisplayName,
|
|
||||||
style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold),
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = if (replied.attachment?.mimeType?.startsWith("image/") == true) "[圖片]" else replied.content,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
maxLines = 1, // 限制一行,避免回覆訊息太長塞爆畫面
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 訊息主體內容(文字或圖片)
|
// 2. 獨立的圖片元件(完全沒有外層氣泡背景色與 Padding)
|
||||||
if (isImage) {
|
AsyncImage(
|
||||||
// 💡 移除外層固定限制,改由 AsyncImage 自己決定尺寸
|
model = finalImageModel,
|
||||||
AsyncImage(
|
contentDescription = "聊天圖片",
|
||||||
model = finalImageModel,
|
contentScale = ContentScale.Fit,
|
||||||
contentDescription = "聊天圖片",
|
modifier = Modifier
|
||||||
// 🌟 關鍵 1:使用 ContentScale.Fit 或 Inside
|
.padding(top = 4.dp)
|
||||||
contentScale = ContentScale.Fit,
|
.sizeIn(maxWidth = 240.dp, maxHeight = 300.dp)
|
||||||
modifier = Modifier
|
.clip(RoundedCornerShape(12.dp)) // 圖片自帶圓角,更美觀
|
||||||
.padding(top = 4.dp)
|
.border(
|
||||||
// 🌟 關鍵 2:限制最大寬高,避免超大圖塞滿螢幕
|
width = 0.5.dp,
|
||||||
.sizeIn(maxWidth = 240.dp, maxHeight = 300.dp)
|
color = Color.LightGray.copy(alpha = 0.4f),
|
||||||
.clip(RoundedCornerShape(8.dp))
|
shape = RoundedCornerShape(12.dp)
|
||||||
.background(Color.LightGray),
|
)
|
||||||
onError = { errorState ->
|
.background(Color(0xFFF5F5F5)),
|
||||||
android.util.Log.e("ChatImageError", "原因: ${errorState.result.throwable}")
|
onError = { errorState ->
|
||||||
}
|
android.util.Log.e("ChatImageError", "原因: ${errorState.result.throwable}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 🌟 文字訊息:維持原本的 Box 氣泡樣式
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 280.dp)
|
||||||
|
.clip(
|
||||||
|
RoundedCornerShape(
|
||||||
|
topStart = 12.dp, topEnd = 12.dp,
|
||||||
|
bottomStart = if (isMe) 12.dp else 0.dp,
|
||||||
|
bottomEnd = if (isMe) 0.dp else 12.dp
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = { onReplyClick(message) },
|
||||||
|
onClick = { onRowClick(message) }
|
||||||
|
)
|
||||||
|
.background(color = finalBubbleColor)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
message.repliedMessage?.let { replied ->
|
||||||
|
RepliedMessagePreview(replied, isMe, partnerDisplayName, currentUserId)
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
|
||||||
Text(
|
Text(
|
||||||
text = rawContent,
|
text = rawContent,
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(
|
style = MaterialTheme.typography.bodyMedium.copy(fontSize = 16.sp)
|
||||||
fontSize = 16.sp
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. 按讚/反應功能(無論文字或圖片都靠下對齊顯示)
|
||||||
if (!message.reactions.isNullOrEmpty()) {
|
if (!message.reactions.isNullOrEmpty()) {
|
||||||
ReactionRow(
|
ReactionRow(
|
||||||
reactions = message.reactions,
|
reactions = message.reactions,
|
||||||
|
|
@ -702,10 +657,40 @@ fun MessageRow(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🛑 如果是對方發的訊息(雖然對方理論上不會有 FAILED 狀態,安全起見做對稱),或是想放右邊的提示
|
|
||||||
// 可以依此類推。通常聊天軟體只會處理自己發送失敗的提示。
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 💡 提取出的「被回覆訊息預覽」組件,避免重複代碼並維持排版乾淨
|
||||||
|
@Composable
|
||||||
|
private fun RepliedMessagePreview(
|
||||||
|
replied: Message,
|
||||||
|
isMe: Boolean,
|
||||||
|
partnerDisplayName: String,
|
||||||
|
currentUserId: String
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 6.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(Color(0x1A000000))
|
||||||
|
.padding(start = 6.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(3.dp)
|
||||||
|
.height(22.dp)
|
||||||
|
.background(if (isMe) Color(0xFF4CAF50) else Color(0xFF78909C))
|
||||||
|
)
|
||||||
|
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||||
|
Text(
|
||||||
|
text = if (replied.attachment?.mimeType?.startsWith("image/") == true) "[圖片]" else replied.content ?: "",
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(fontSize = 14.sp),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue