dm-44
This commit is contained in:
parent
4f187feab2
commit
d82884b1d3
1 changed files with 127 additions and 86 deletions
|
|
@ -5,9 +5,11 @@ import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -31,6 +33,7 @@ import androidx.compose.material.icons.automirrored.filled.Send
|
||||||
import androidx.compose.material.icons.filled.AccountCircle
|
import androidx.compose.material.icons.filled.AccountCircle
|
||||||
import androidx.compose.material.icons.filled.AddCircle
|
import androidx.compose.material.icons.filled.AddCircle
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Send
|
import androidx.compose.material.icons.filled.Send
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
|
@ -53,10 +56,14 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
|
@ -71,6 +78,7 @@ import com.example.login_v3.home.Message.ViewModel.UserStatus
|
||||||
import com.example.login_v3.navigation.BottomBarViewModel
|
import com.example.login_v3.navigation.BottomBarViewModel
|
||||||
import com.example.login_v3.navigation.Screen
|
import com.example.login_v3.navigation.Screen
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageMessaging(
|
fun MessageMessaging(
|
||||||
|
|
@ -86,6 +94,9 @@ fun MessageMessaging(
|
||||||
val sendStatus by viewModel.sendMessageState.collectAsStateWithLifecycle()
|
val sendStatus by viewModel.sendMessageState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
//reply function
|
||||||
|
val replyingMessage by viewModel.replyingMessage.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
//進入時重整
|
//進入時重整
|
||||||
LaunchedEffect(roomId) {
|
LaunchedEffect(roomId) {
|
||||||
viewModel.loadMessages(roomId)
|
viewModel.loadMessages(roomId)
|
||||||
|
|
@ -168,13 +179,15 @@ fun MessageMessaging(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
// 💡 這裡把 replyingMessage 與 取消回覆的動作 傳進去 (等等修改 MessageInputBar)
|
||||||
MessageInputBar(
|
MessageInputBar(
|
||||||
isLoading = sendStatus is SendMessageState.Loading,
|
isLoading = sendStatus is SendMessageState.Loading,
|
||||||
|
replyingMessage = replyingMessage,
|
||||||
|
onCancelReply = { viewModel.setReplyingMessage(null) },
|
||||||
onSendClick = { text ->
|
onSendClick = { text ->
|
||||||
viewModel.sendMessage(roomId = roomId, content = text)
|
viewModel.sendMessage(roomId = roomId, content = text)
|
||||||
},
|
},
|
||||||
onImageSelected = { uri ->
|
onImageSelected = { uri ->
|
||||||
// ✨ 這裡呼叫你先前在 ViewModel 寫好的上傳圖片功能
|
|
||||||
viewModel.uploadAttachment(roomId = roomId, fileUri = uri)
|
viewModel.uploadAttachment(roomId = roomId, fileUri = uri)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -200,7 +213,8 @@ fun MessageMessaging(
|
||||||
currentUserId = state.currentUserId,
|
currentUserId = state.currentUserId,
|
||||||
messages = state.messages,
|
messages = state.messages,
|
||||||
partnerAvatarUrl = state.partnerAvatarUrl,
|
partnerAvatarUrl = state.partnerAvatarUrl,
|
||||||
partnerDisplayName = state.roomTitle
|
partnerDisplayName = state.roomTitle,
|
||||||
|
onReplyClick = { message -> viewModel.setReplyingMessage(message) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -221,6 +235,7 @@ fun MessageList(
|
||||||
partnerAvatarUrl: Any?,
|
partnerAvatarUrl: Any?,
|
||||||
partnerDisplayName: String,
|
partnerDisplayName: String,
|
||||||
messages: List<Message>,
|
messages: List<Message>,
|
||||||
|
onReplyClick: (Message) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|
@ -236,7 +251,8 @@ fun MessageList(
|
||||||
partnerAvatarUrl = partnerAvatarUrl,
|
partnerAvatarUrl = partnerAvatarUrl,
|
||||||
partnerDisplayName = partnerDisplayName,
|
partnerDisplayName = partnerDisplayName,
|
||||||
isMe = isMe,
|
isMe = isMe,
|
||||||
currentUserId = currentUserId
|
currentUserId = currentUserId,
|
||||||
|
onReplyClick = onReplyClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -248,38 +264,32 @@ fun MessageRow(
|
||||||
isMe: Boolean,
|
isMe: Boolean,
|
||||||
partnerAvatarUrl: Any?,
|
partnerAvatarUrl: Any?,
|
||||||
partnerDisplayName: String,
|
partnerDisplayName: String,
|
||||||
currentUserId: String
|
currentUserId: String,
|
||||||
|
onReplyClick: (Message) -> Unit // 💡 新增參數
|
||||||
) {
|
) {
|
||||||
// 1. ✨ 使用新的 attachment 欄位精準判斷是否為圖片,或是舊格式的網址判斷(相容舊資料)
|
|
||||||
val isAttachmentImage = message.attachment?.mimeType?.startsWith("image/") == true
|
val isAttachmentImage = message.attachment?.mimeType?.startsWith("image/") == true
|
||||||
|
|
||||||
// 保留你原本的舊資料防禦邏輯,避免舊的訊息爆掉
|
|
||||||
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")
|
||||||
val isLegacyImage = contentLowerCase.startsWith("http") &&
|
val isLegacyImage = contentLowerCase.startsWith("http") && imageExtensions.any { contentLowerCase.contains(it) }
|
||||||
imageExtensions.any { contentLowerCase.contains(it) }
|
|
||||||
|
|
||||||
val isImage = isAttachmentImage || isLegacyImage
|
val isImage = isAttachmentImage || isLegacyImage
|
||||||
|
|
||||||
// 2. 補全圖片網址
|
|
||||||
// 2. 補全圖片網址
|
|
||||||
val rawContent = message.content ?: ""
|
val rawContent = message.content ?: ""
|
||||||
val finalImageModel = if (rawContent.startsWith("/")) {
|
val finalImageModel = if (rawContent.startsWith("/")) "http://192.168.0.217$rawContent" else rawContent
|
||||||
"http://192.168.0.217$rawContent"
|
|
||||||
} else {
|
|
||||||
rawContent
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
// 💡 讓使用者長按整條訊息就能觸發回覆
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = { onReplyClick(message) },
|
||||||
|
onClick = { /* 可做其他事或留空 */ }
|
||||||
|
),
|
||||||
horizontalArrangement = if (isMe) Arrangement.End else Arrangement.Start
|
horizontalArrangement = if (isMe) Arrangement.End else Arrangement.Start
|
||||||
) {
|
) {
|
||||||
if (!isMe) {
|
if (!isMe) {
|
||||||
UserAvatar(
|
UserAvatar(
|
||||||
avatarUrl = partnerAvatarUrl,
|
avatarUrl = partnerAvatarUrl,
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(end = 8.dp).size(32.dp)
|
||||||
.padding(end = 8.dp)
|
|
||||||
.size(32.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,8 +299,7 @@ fun MessageRow(
|
||||||
.background(
|
.background(
|
||||||
color = if (isMe) Color(0xFFDCF8C6) else Color(0xFFECEFF1),
|
color = if (isMe) Color(0xFFDCF8C6) else Color(0xFFECEFF1),
|
||||||
shape = RoundedCornerShape(
|
shape = RoundedCornerShape(
|
||||||
topStart = 12.dp,
|
topStart = 12.dp, topEnd = 12.dp,
|
||||||
topEnd = 12.dp,
|
|
||||||
bottomStart = if (isMe) 12.dp else 0.dp,
|
bottomStart = if (isMe) 12.dp else 0.dp,
|
||||||
bottomEnd = if (isMe) 0.dp else 12.dp
|
bottomEnd = if (isMe) 0.dp else 12.dp
|
||||||
)
|
)
|
||||||
|
|
@ -298,6 +307,50 @@ fun MessageRow(
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
|
// 💡 【核心:渲染被回覆的訊息內容】
|
||||||
|
message.repliedMessage?.let { replied ->
|
||||||
|
val repliedSenderName = if (replied.senderId == currentUserId) "你" else partnerDisplayName
|
||||||
|
|
||||||
|
// 被回覆的小氣泡外框
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 6.dp)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
// 💡 修正這裡:直接根據 isMe 決定傳入哪一個 Color 物件即可
|
||||||
|
.background(if (isMe) Color(0xFFC7EBB2) else Color(0xFFE0E0E0))
|
||||||
|
.drawBehind {
|
||||||
|
// 在左側畫一條精緻的提示線 (類似 LINE / Telegram)
|
||||||
|
drawLine(
|
||||||
|
color = Color.Gray,
|
||||||
|
start = Offset(0f, 0f),
|
||||||
|
end = Offset(0f, size.height),
|
||||||
|
strokeWidth = 6f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(start = 8.dp, top = 4.dp, bottom = 4.dp, end = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = repliedSenderName,
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold),
|
||||||
|
color = Color(0xFF1E88E5),
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
// 如果被回覆的是圖片,顯示 [圖片],否則顯示文字內容
|
||||||
|
text = if (replied.attachment?.mimeType?.startsWith("image/") == true) "[圖片]" else replied.content,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.DarkGray,
|
||||||
|
maxLines = 1, // 最多一行,避免太長
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 原本的訊息主體內容(文字或圖片)
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -313,7 +366,6 @@ fun MessageRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(max = 300.dp)
|
.heightIn(max = 300.dp)
|
||||||
.clickable { /* 點擊放大 */ }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -331,77 +383,66 @@ fun MessageRow(
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageInputBar(
|
fun MessageInputBar(
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
|
replyingMessage: Message?, // 💡 新增
|
||||||
|
onCancelReply: () -> Unit, // 💡 新增
|
||||||
onSendClick: (String) -> Unit,
|
onSendClick: (String) -> Unit,
|
||||||
onImageSelected: (Uri) -> Unit, // ✨ 新增:選取圖片後的回呼
|
onImageSelected: (Uri) -> Unit
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
) {
|
||||||
var textInput by remember { mutableStateOf("") }
|
var textState by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val imagePickerLauncher = rememberLauncherForActivityResult(
|
Column(
|
||||||
contract = ActivityResultContracts.GetContent()
|
modifier = Modifier
|
||||||
) { uri: Uri? ->
|
.fillMaxWidth()
|
||||||
if (uri != null) {
|
.background(Color.White) // 或者是你的背景色
|
||||||
onImageSelected(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
modifier = modifier.fillMaxWidth()
|
|
||||||
) {
|
) {
|
||||||
|
// 💡 如果目前處於回覆狀態,就在輸入框上方橫向塞一個預覽 UI
|
||||||
|
AnimatedVisibility(visible = replyingMessage != null) {
|
||||||
|
if (replyingMessage != null) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
.background(Color(0xFFF5F5F5))
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = "正在回覆訊息",
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold),
|
||||||
|
color = Color(0xFFDA7029)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = replyingMessage.content,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.Gray,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 取消回覆的 X 按鈕
|
||||||
|
IconButton(onClick = onCancelReply, modifier = Modifier.size(24.dp)) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "取消回覆",
|
||||||
|
tint = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下方維持你原本既有的輸入框 Row 排版
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
// 你原本的 TextField, 選擇圖片按鈕, 送出按鈕...
|
||||||
// ✨ 新增:相簿按鈕
|
// 送出時呼叫:
|
||||||
IconButton(
|
// onSendClick(textState)
|
||||||
onClick = { imagePickerLauncher.launch("image/*") },
|
// textState = ""
|
||||||
enabled = !isLoading
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.AddCircle, // 你也可以改用 Icons.Default.Image
|
|
||||||
contentDescription = "選擇圖片",
|
|
||||||
tint = Color(0xFFDA7029)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TextField(
|
|
||||||
value = textInput,
|
|
||||||
onValueChange = { textInput = it },
|
|
||||||
placeholder = { Text("輸入訊息...") },
|
|
||||||
modifier = Modifier.weight(1.0f),
|
|
||||||
colors = TextFieldDefaults.colors(
|
|
||||||
focusedContainerColor = Color.Transparent,
|
|
||||||
unfocusedContainerColor = Color.Transparent,
|
|
||||||
disabledContainerColor = Color.Transparent,
|
|
||||||
focusedIndicatorColor = Color.Transparent,
|
|
||||||
unfocusedIndicatorColor = Color.Transparent
|
|
||||||
),
|
|
||||||
maxLines = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (textInput.isNotBlank() && !isLoading) {
|
|
||||||
onSendClick(textInput)
|
|
||||||
textInput = ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled = textInput.isNotBlank() && !isLoading
|
|
||||||
) {
|
|
||||||
if (isLoading) {
|
|
||||||
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
|
||||||
} else {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Send,
|
|
||||||
contentDescription = "發送訊息",
|
|
||||||
tint = if (textInput.isNotBlank()) Color(0xFFDA7029) else Color.Gray
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue