This commit is contained in:
Eric85710 2026-06-04 16:29:56 +08:00
parent 9fda848948
commit cce155057d
2 changed files with 161 additions and 66 deletions

View file

@ -44,6 +44,7 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -68,6 +69,7 @@ import coil.compose.AsyncImage
import com.example.login_v3.R import com.example.login_v3.R
import com.example.login_v3.data.api.api_class.Message import com.example.login_v3.data.api.api_class.Message
import com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail.MessageActionMenuRow import com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail.MessageActionMenuRow
import com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail.MessageEmojiBar
import com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail.MessageInputBar import com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail.MessageInputBar
import com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail.ReactionRow import com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail.ReactionRow
import com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail.UserAvatar import com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail.UserAvatar
@ -111,6 +113,18 @@ fun MessageMessaging(
//message that selected //message that selected
val actionMessage by viewModel.actionMessage.collectAsStateWithLifecycle() val actionMessage by viewModel.actionMessage.collectAsStateWithLifecycle()
var emojiTargetMessage by remember { mutableStateOf<Message?>(null) }
// 🌟 核心:使用 derivedStateOf 統一控管狀態權限(長按優先權通常大於單擊)
val bottomBarState by remember {
derivedStateOf {
when {
actionMessage != null -> BottomBarState.ActionMenu(actionMessage!!)
emojiTargetMessage != null -> BottomBarState.EmojiMenu(emojiTargetMessage!!)
else -> BottomBarState.Input
}
}
}
//進入時重整 //進入時重整
LaunchedEffect(roomId) { LaunchedEffect(roomId) {
@ -194,39 +208,60 @@ fun MessageMessaging(
) )
}, },
bottomBar = { bottomBar = {
// 👈 直接將 actionMessage 作為 targetState // 👈 使用我們定義的 sealed interface 狀態機
AnimatedContent( AnimatedContent(
targetState = actionMessage, targetState = bottomBarState,
label = "BottomBarSwitchAnimation" label = "BottomBarSwitchAnimation"
) { currentActionMessage -> ) { currentState ->
if (currentActionMessage != null) { when (currentState) {
is BottomBarState.Input -> {
MessageInputBar(
isLoading = sendStatus is SendMessageState.Loading,
replyingMessage = replyingMessage,
onCancelReply = { viewModel.setReplyingMessage(null) },
onSendClick = { content -> viewModel.sendMessage(roomId, content) },
onImageSelected = { uri -> viewModel.uploadAttachment(roomId, uri) }
)
}
// 💡 核心修正:從目前的 uiState 中安全地取出當前使用者的 ID 來做比較 is BottomBarState.ActionMenu -> {
val currentUserId = (uiState as? MessagesUiState.Success)?.currentUserId val currentActionMessage = currentState.message
val isOwnMessage = currentActionMessage.senderId == currentUserId val currentUserId = (uiState as? MessagesUiState.Success)?.currentUserId
val isOwnMessage = currentActionMessage.senderId == currentUserId
// 顯示動作選單 MessageActionMenuRow(
MessageActionMenuRow( message = currentActionMessage,
message = currentActionMessage, isOwnMessage = isOwnMessage,
isOwnMessage = isOwnMessage, // 👈 記得要把這個新參數帶進去! onCancel = { viewModel.clearActionMessage() },
onCancel = { viewModel.clearActionMessage() }, onReplyClick = { msg ->
onReplyClick = { msg -> viewModel.setReplyingMessage(msg)
viewModel.setReplyingMessage(msg) viewModel.clearActionMessage()
viewModel.clearActionMessage() },
}, onDeleteClick = { msg ->
onDeleteClick = { msg -> viewModel.deleteMessage(roomId, msg.id)
viewModel.deleteMessage(roomId, msg.id) }
} )
) }
} else {
// 顯示原本的輸入框 is BottomBarState.EmojiMenu -> {
MessageInputBar( // 👈 渲染全新設計的獨立 Emoji Bar
isLoading = sendStatus is SendMessageState.Loading, val currentEmojiMessage = currentState.message
replyingMessage = replyingMessage, MessageEmojiBar(
onCancelReply = { viewModel.setReplyingMessage(null) }, // 🌟 移除了 commonEmojis 參數,變得非常清爽!
onSendClick = { content -> viewModel.sendMessage(roomId, content) }, onEmojiClick = { emoji ->
onImageSelected = { uri -> viewModel.uploadAttachment(roomId, uri) } val targetReaction = currentEmojiMessage.reactions?.find { it.emoji == emoji }
) if (targetReaction?.meReacted == true) {
viewModel.removeMessageReaction(roomId, currentEmojiMessage.id, emoji)
} else {
viewModel.addMessageReaction(roomId, currentEmojiMessage.id, emoji)
}
emojiTargetMessage = null // 點完後自動關閉
},
onCancel = {
emojiTargetMessage = null // 點擊關閉
}
)
}
} }
} }
} }
@ -252,35 +287,23 @@ fun MessageMessaging(
messages = state.messages, messages = state.messages,
partnerAvatarUrl = state.partnerAvatarUrl, partnerAvatarUrl = state.partnerAvatarUrl,
partnerDisplayName = state.roomTitle, partnerDisplayName = state.roomTitle,
onReplyClick = { message -> viewModel.setActionMessage(message) }, onReplyClick = { message ->
onReactionClick = { message, emoji -> emojiTargetMessage = null
// 尋找該訊息內,對應點擊到的 Reaction 物件 viewModel.setActionMessage(message)
val targetReaction = message.reactions?.find { it.emoji == emoji }
if (targetReaction?.meReacted == true) {
// 我自己已經點過了 ➡️ 觸發「移除/收回」
viewModel.removeMessageReaction(
roomId = roomId,
messageId = message.id,
emoji = emoji
)
} else {
// 我沒點過(別人點的) ➡️ 觸發「新增/一起點」
viewModel.addMessageReaction(
roomId = roomId,
messageId = message.id,
emoji = emoji
)
}
}, },
// 💡 單擊事件:開啟 Emoji 選單,同時清空長按選單
// 點選彈出面板(Panel)的新 Emoji onRowClick = { message ->
onAddReaction = { message, emoji -> viewModel.clearActionMessage()
viewModel.addMessageReaction( emojiTargetMessage = message
roomId = roomId, },
messageId = message.id, onReactionClick = { message, emoji ->
emoji = emoji // 這是訊息氣泡下方既有 Reaction 小標籤的點擊事件,保持原樣即可
) val targetReaction = message.reactions?.find { it.emoji == emoji }
if (targetReaction?.meReacted == true) {
viewModel.removeMessageReaction(roomId, message.id, emoji)
} else {
viewModel.addMessageReaction(roomId, message.id, emoji)
}
} }
) )
} }
@ -304,7 +327,7 @@ fun MessageList(
messages: List<Message>, messages: List<Message>,
onReplyClick: (Message) -> Unit, onReplyClick: (Message) -> Unit,
onReactionClick: (Message, String) -> Unit, onReactionClick: (Message, String) -> Unit,
onAddReaction: (Message, String) -> Unit, onRowClick: (Message) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LazyColumn( LazyColumn(
@ -323,7 +346,7 @@ fun MessageList(
currentUserId = currentUserId, currentUserId = currentUserId,
onReplyClick = onReplyClick, onReplyClick = onReplyClick,
onReactionClick = onReactionClick, onReactionClick = onReactionClick,
onAddReaction = onAddReaction onRowClick = onRowClick
) )
} }
} }
@ -339,7 +362,7 @@ fun MessageRow(
currentUserId: String, currentUserId: String,
onReplyClick: (Message) -> Unit, onReplyClick: (Message) -> Unit,
onReactionClick: (Message, String) -> Unit, onReactionClick: (Message, String) -> Unit,
onAddReaction: (Message, String) -> Unit onRowClick: (Message) -> Unit,
) { ) {
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()
@ -390,8 +413,8 @@ fun MessageRow(
.fillMaxWidth() .fillMaxWidth()
// 💡 讓使用者長按整條訊息就能觸發回覆 // 💡 讓使用者長按整條訊息就能觸發回覆
.combinedClickable( .combinedClickable(
onLongClick = { onReplyClick(message) }, onLongClick = { onReplyClick(message) }, // 長按 -> 喚起 ActionMenu
onClick = { showEmojiPanel = true } // 💡 點一下開啟 Emoji 面板 onClick = { onRowClick(message) } // 單擊 -> 喚起 EmojiMenu
), ),
horizontalArrangement = if (isMe) Arrangement.End else Arrangement.Start horizontalArrangement = if (isMe) Arrangement.End else Arrangement.Start
) { ) {
@ -511,10 +534,6 @@ fun MessageRow(
fontSize = 24.sp, fontSize = 24.sp,
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.clickable {
onAddReaction(message, emoji) // 觸發新增 Reaction API
showEmojiPanel = false // 關閉面板
}
.padding(4.dp) .padding(4.dp)
) )
} }

View file

@ -0,0 +1,76 @@
package com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun MessageEmojiBar(
onEmojiClick: (String) -> Unit,
onCancel: () -> Unit,
modifier: Modifier = Modifier
) {
val emojiList = remember {
listOf(
"👍", "❤️", "😂", "😮", "😢", "🙏",
"🔥", "🎉", "👏", "👀", "✨", "💯",
"🤔", "🥳", "😎", "🚀", "💔", "💩"
)
}
Surface(
modifier = modifier.fillMaxWidth(),
color = Color(0xFFF5F5F5), // 稍微與純白底色做點區隔,可自行調整
tonalElevation = 4.dp
) {
Row(
modifier = Modifier
.navigationBarsPadding() // 避免被手機底部的導覽橫條遮擋
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
// 左側:Emoji 點擊區
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.weight(1f)
) {
emojiList.forEach { emoji ->
Text(
text = emoji,
fontSize = 26.sp,
modifier = Modifier
.clip(CircleShape)
.clickable { onEmojiClick(emoji) }
.padding(4.dp)
)
}
}
// 右側:關閉按鈕 (Icon 可以換成你專案內現有的)
Box(
modifier = Modifier
.clip(CircleShape)
.clickable { onCancel() }
.padding(4.dp)
) {
Text(text = "✕", color = Color.Gray, fontSize = 18.sp)
}
}
}
}