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.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -68,6 +69,7 @@ import coil.compose.AsyncImage
import com.example.login_v3.R
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.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.ReactionRow
import com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail.UserAvatar
@ -111,6 +113,18 @@ fun MessageMessaging(
//message that selected
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) {
@ -194,39 +208,60 @@ fun MessageMessaging(
)
},
bottomBar = {
// 👈 直接將 actionMessage 作為 targetState
// 👈 使用我們定義的 sealed interface 狀態機
AnimatedContent(
targetState = actionMessage,
targetState = bottomBarState,
label = "BottomBarSwitchAnimation"
) { currentActionMessage ->
if (currentActionMessage != null) {
) { currentState ->
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 來做比較
val currentUserId = (uiState as? MessagesUiState.Success)?.currentUserId
val isOwnMessage = currentActionMessage.senderId == currentUserId
is BottomBarState.ActionMenu -> {
val currentActionMessage = currentState.message
val currentUserId = (uiState as? MessagesUiState.Success)?.currentUserId
val isOwnMessage = currentActionMessage.senderId == currentUserId
// 顯示動作選單
MessageActionMenuRow(
message = currentActionMessage,
isOwnMessage = isOwnMessage, // 👈 記得要把這個新參數帶進去!
onCancel = { viewModel.clearActionMessage() },
onReplyClick = { msg ->
viewModel.setReplyingMessage(msg)
viewModel.clearActionMessage()
},
onDeleteClick = { msg ->
viewModel.deleteMessage(roomId, msg.id)
}
)
} else {
// 顯示原本的輸入框
MessageInputBar(
isLoading = sendStatus is SendMessageState.Loading,
replyingMessage = replyingMessage,
onCancelReply = { viewModel.setReplyingMessage(null) },
onSendClick = { content -> viewModel.sendMessage(roomId, content) },
onImageSelected = { uri -> viewModel.uploadAttachment(roomId, uri) }
)
MessageActionMenuRow(
message = currentActionMessage,
isOwnMessage = isOwnMessage,
onCancel = { viewModel.clearActionMessage() },
onReplyClick = { msg ->
viewModel.setReplyingMessage(msg)
viewModel.clearActionMessage()
},
onDeleteClick = { msg ->
viewModel.deleteMessage(roomId, msg.id)
}
)
}
is BottomBarState.EmojiMenu -> {
// 👈 渲染全新設計的獨立 Emoji Bar
val currentEmojiMessage = currentState.message
MessageEmojiBar(
// 🌟 移除了 commonEmojis 參數,變得非常清爽!
onEmojiClick = { emoji ->
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,
partnerAvatarUrl = state.partnerAvatarUrl,
partnerDisplayName = state.roomTitle,
onReplyClick = { message -> viewModel.setActionMessage(message) },
onReactionClick = { message, emoji ->
// 尋找該訊息內,對應點擊到的 Reaction 物件
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
)
}
onReplyClick = { message ->
emojiTargetMessage = null
viewModel.setActionMessage(message)
},
// 點選彈出面板(Panel)的新 Emoji
onAddReaction = { message, emoji ->
viewModel.addMessageReaction(
roomId = roomId,
messageId = message.id,
emoji = emoji
)
// 💡 單擊事件:開啟 Emoji 選單,同時清空長按選單
onRowClick = { message ->
viewModel.clearActionMessage()
emojiTargetMessage = message
},
onReactionClick = { message, 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>,
onReplyClick: (Message) -> Unit,
onReactionClick: (Message, String) -> Unit,
onAddReaction: (Message, String) -> Unit,
onRowClick: (Message) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
@ -323,7 +346,7 @@ fun MessageList(
currentUserId = currentUserId,
onReplyClick = onReplyClick,
onReactionClick = onReactionClick,
onAddReaction = onAddReaction
onRowClick = onRowClick
)
}
}
@ -339,7 +362,7 @@ fun MessageRow(
currentUserId: String,
onReplyClick: (Message) -> Unit,
onReactionClick: (Message, String) -> Unit,
onAddReaction: (Message, String) -> Unit
onRowClick: (Message) -> Unit,
) {
val isAttachmentImage = message.attachment?.mimeType?.startsWith("image/") == true
val contentLowerCase = (message.content ?: "").lowercase()
@ -390,8 +413,8 @@ fun MessageRow(
.fillMaxWidth()
// 💡 讓使用者長按整條訊息就能觸發回覆
.combinedClickable(
onLongClick = { onReplyClick(message) },
onClick = { showEmojiPanel = true } // 💡 點一下開啟 Emoji 面板
onLongClick = { onReplyClick(message) }, // 長按 -> 喚起 ActionMenu
onClick = { onRowClick(message) } // 單擊 -> 喚起 EmojiMenu
),
horizontalArrangement = if (isMe) Arrangement.End else Arrangement.Start
) {
@ -511,10 +534,6 @@ fun MessageRow(
fontSize = 24.sp,
modifier = Modifier
.clip(CircleShape)
.clickable {
onAddReaction(message, emoji) // 觸發新增 Reaction API
showEmojiPanel = false // 關閉面板
}
.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)
}
}
}
}