From 1ecbc7b15608aef755c6434cf60ee4a7e0c7806c Mon Sep 17 00:00:00 2001 From: Eric85710 Date: Sun, 31 May 2026 15:26:10 +0800 Subject: [PATCH] dm-68 --- .../Message/UI/Detail/Message_Messaging.kt | 243 +++--------------- .../MessageActionMenuRow.kt | 150 +++++++++++ .../MessageInputBar.kt | 163 ++++++++++++ .../Message_Messaging_detail/ReactionRow.kt | 60 +++++ .../Message_Messaging_detail/UserAvatar.kt | 26 ++ 5 files changed, 428 insertions(+), 214 deletions(-) create mode 100644 app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/MessageActionMenuRow.kt create mode 100644 app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/MessageInputBar.kt create mode 100644 app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/ReactionRow.kt create mode 100644 app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/UserAvatar.kt diff --git a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging.kt b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging.kt index e284f48..038e40a 100644 --- a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging.kt +++ b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging.kt @@ -1,26 +1,20 @@ package com.example.login_v3.home.Message.UI.Detail import android.net.Uri -import android.util.Log import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding @@ -32,12 +26,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.Send -import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.AddCircle -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Photo import androidx.compose.material.icons.filled.Send @@ -47,15 +35,10 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -82,14 +65,16 @@ import androidx.navigation.NavController 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.data.api.api_class.Reaction +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.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 import com.example.login_v3.home.Message.ViewModel.Detail.ChatViewModel import com.example.login_v3.home.Message.ViewModel.Detail.MessagesUiState -import com.example.login_v3.home.Message.ViewModel.Detail.ReactionUsersState 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.Screen + @OptIn(ExperimentalMaterial3Api::class) @@ -199,18 +184,29 @@ fun MessageMessaging( ) }, bottomBar = { - // 💡 這裡把 replyingMessage 與 取消回覆的動作 傳進去 (等等修改 MessageInputBar) - MessageInputBar( - isLoading = sendStatus is SendMessageState.Loading, - replyingMessage = replyingMessage, - onCancelReply = { viewModel.setReplyingMessage(null) }, - onSendClick = { text -> - viewModel.sendMessage(roomId = roomId, content = text) - }, - onImageSelected = { uri -> - viewModel.uploadAttachment(roomId = roomId, fileUri = uri) - } - ) + if (actionMessage != null) { + // 顯示動作選單 + MessageActionMenuRow( + message = actionMessage!!, + onCancel = { viewModel.clearActionMessage() }, // 👈 呼叫 ViewModel 的清除 + 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) }, // 傳 null 等同清除 + onSendClick = { content -> viewModel.sendMessage(roomId, content) }, + onImageSelected = { uri -> viewModel.uploadAttachment(roomId, uri) } + ) + } } ) { innerPadding -> Box( @@ -234,7 +230,7 @@ fun MessageMessaging( messages = state.messages, partnerAvatarUrl = state.partnerAvatarUrl, partnerDisplayName = state.roomTitle, - onReplyClick = { message -> viewModel.setReplyingMessage(message) }, + onReplyClick = { message -> viewModel.setActionMessage(message) }, onReactionClick = { message, emoji -> // 尋找該訊息內,對應點擊到的 Reaction 物件 val targetReaction = message.reactions?.find { it.emoji == emoji } @@ -474,185 +470,4 @@ fun MessageRow( } } } -} - - -@Composable -fun MessageInputBar( - isLoading: Boolean, - replyingMessage: Message?, - onCancelReply: () -> Unit, - onSendClick: (String) -> Unit, - onImageSelected: (Uri) -> Unit -) { - var textState by remember { mutableStateOf("") } - - val imagePickerLauncher = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent() - ) { uri: Uri? -> - uri?.let { onImageSelected(it) } - } - - // 💡 外層包裹一層 Box,並加上 padding 與 navigationBarsPadding,確保懸浮且不被系統列擋住 - Box( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() // 自動適應 Android 系統導覽列高度 - .padding(horizontal = 16.dp, vertical = 8.dp) // 懸浮的外邊距 - ) { - // 使用 Surface 或 Card 來實作圓角與陰影懸浮感 - Surface( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), // 整體大圓角化 - color = Color.White, - tonalElevation = 6.dp, // 頂級懸浮感 - shadowElevation = 6.dp // 實體陰影 - ) { - Column(modifier = Modifier.fillMaxWidth()) { - - // 1. 回覆預覽列 (加入頂部圓角修飾) - AnimatedVisibility(visible = replyingMessage != null) { - if (replyingMessage != null) { - Row( - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFFF8F9FA)) // 更輕盈的底色 - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = replyingMessage.content, - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - IconButton(onClick = onCancelReply, modifier = Modifier.size(24.dp)) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "取消回覆", - tint = Color.Gray - ) - } - } - } - } - - // 2. 實際的輸入工具列 - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // 圖片選擇按鈕 - IconButton(onClick = { imagePickerLauncher.launch("image/*") }) { - Icon( - imageVector = Icons.Default.Photo, - contentDescription = "選擇圖片", - tint = Color.Gray - ) - } - - // 輸入文字框 (改為內嵌樣式,去掉自帶的邊框,讓整體更有一體感) - BasicTextField( - value = textState, - onValueChange = { textState = it }, - modifier = Modifier - .weight(1f) - .padding(horizontal = 8.dp), - textStyle = MaterialTheme.typography.bodyLarge, - decorationBox = { innerTextField -> - if (textState.isEmpty()) { - Text("請輸入訊息...", color = Color.LightGray) - } - innerTextField() - } - ) - - // 送出按鈕 - IconButton( - onClick = { - if (textState.isNotBlank() && !isLoading) { - onSendClick(textState) - textState = "" - } - }, - enabled = textState.isNotBlank() && !isLoading - ) { - if (isLoading) { - CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp) - } else { - // 加上小背景讓送出按鈕更有重點 - val iconColor = if (textState.isNotBlank()) Color(0xFFDA7029) else Color.LightGray - Icon( - imageVector = Icons.Default.Send, - contentDescription = "送出", - tint = iconColor - ) - } - } - } - } - } - } -} - - -@Composable -fun UserAvatar( - avatarUrl: Any?, // ✨ 改成 Any? - modifier: Modifier = Modifier -) { - AsyncImage( - model = avatarUrl ?: R.drawable.avatar_v1, // 如果為 null,就用預設圖 - contentDescription = "用戶頭像", - modifier = modifier - .background(Color.LightGray, shape = CircleShape) - .clip(CircleShape), - contentScale = ContentScale.Crop - ) -} - - -@Composable -fun ReactionRow( - reactions: List, - onReactionClick: (String) -> Unit -) { - // 使用 FlowRow,如果貼圖太多會自動折行 - OptIn(ExperimentalLayoutApi::class) - FlowRow( - modifier = Modifier.padding(top = 4.dp, start = 4.dp, end = 4.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - reactions.forEach { reaction -> - if (reaction.count > 0) { - Row( - modifier = Modifier - .clip(CircleShape) - .background( - // 如果我自己有按讚,背景變稍微深色/藍色點綴,否則為淡灰色 - if (reaction.meReacted) Color(0xFFBBDEFB) else Color(0xFFEEEEEE) - ) - .clickable { onReactionClick(reaction.emoji) } - .padding(horizontal = 8.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text(text = reaction.emoji, fontSize = 14.sp) - Text( - text = reaction.count.toString(), - style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold), - color = if (reaction.meReacted) Color(0xFF1976D2) else Color.DarkGray - ) - } - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/MessageActionMenuRow.kt b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/MessageActionMenuRow.kt new file mode 100644 index 0000000..77fefa1 --- /dev/null +++ b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/MessageActionMenuRow.kt @@ -0,0 +1,150 @@ +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.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Reply +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import com.example.login_v3.data.api.api_class.Message + +@Composable +fun MessageActionMenuRow( + message: Message, + onCancel: () -> Unit, + onReplyClick: (Message) -> Unit, // 👈 修正:這裡只需要定義型態 + onDeleteClick: (Message) -> Unit, + modifier: Modifier = Modifier +) { + // 💡 取得系統剪貼簿,用來實作「複製文字」功能 + val clipboardManager = LocalClipboardManager.current + + Box( + modifier = modifier + .fillMaxWidth() + .navigationBarsPadding() // 自動適應 Android 系統導覽列(手勢列)高度 + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), // 保持懸浮圓角感 + color = Color.White, + tonalElevation = 6.dp, + shadowElevation = 6.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + // 左側:取消關閉選單按鈕 + IconButton(onClick = onCancel) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "取消選取", + tint = Color.Gray + ) + } + + // 中間:各個功能按鈕 + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 1. 回覆 + ChatMenuButton( + imageVector = Icons.Default.Reply, + label = "回覆", + onClick = { onReplyClick(message) } + ) + + // 2. 複製文字 + val isImage = message.content?.startsWith("http") == true || message.attachment != null + ChatMenuButton( + imageVector = Icons.Default.ContentCopy, + label = "複製", + enabled = !isImage, + onClick = { + message.content?.let { text -> + clipboardManager.setText(AnnotatedString(text)) + } + onCancel() + } + ) + + // 3. 刪除 + ChatMenuButton( + imageVector = Icons.Default.Delete, + label = "刪除", + tint = MaterialTheme.colorScheme.error, + onClick = { onDeleteClick(message) } + ) + } + + // 為了讓中間的按鈕群置中,右邊放一個等寬的 Spacer + Spacer(modifier = Modifier.size(48.dp)) + } + } + } +} + +// 💡 記得一併附上這個按鈕元件,它也是放在同一個檔案底部 +@Composable +fun ChatMenuButton( + imageVector: ImageVector, + label: String, + enabled: Boolean = true, + tint: Color = Color(0xFFDA7029), + onClick: () -> Unit +) { + val contentColor = if (enabled) tint else Color.LightGray + val textColor = if (enabled) Color.DarkGray else Color.LightGray + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable(enabled = enabled) { onClick() } + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Icon( + imageVector = imageVector, + contentDescription = label, + tint = contentColor, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = textColor + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/MessageInputBar.kt b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/MessageInputBar.kt new file mode 100644 index 0000000..ef71dff --- /dev/null +++ b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/MessageInputBar.kt @@ -0,0 +1,163 @@ +package com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Photo +import androidx.compose.material.icons.filled.Send +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.example.login_v3.data.api.api_class.Message + +@Composable +fun MessageInputBar( + isLoading: Boolean, + replyingMessage: Message?, + onCancelReply: () -> Unit, + onSendClick: (String) -> Unit, + onImageSelected: (Uri) -> Unit +) { + var textState by remember { mutableStateOf("") } + + val imagePickerLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent() + ) { uri: Uri? -> + uri?.let { onImageSelected(it) } + } + + // 💡 外層包裹一層 Box,並加上 padding 與 navigationBarsPadding,確保懸浮且不被系統列擋住 + Box( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() // 自動適應 Android 系統導覽列高度 + .padding(horizontal = 16.dp, vertical = 8.dp) // 懸浮的外邊距 + ) { + // 使用 Surface 或 Card 來實作圓角與陰影懸浮感 + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), // 整體大圓角化 + color = Color.White, + tonalElevation = 6.dp, // 頂級懸浮感 + shadowElevation = 6.dp // 實體陰影 + ) { + Column(modifier = Modifier.fillMaxWidth()) { + + // 1. 回覆預覽列 (加入頂部圓角修飾) + AnimatedVisibility(visible = replyingMessage != null) { + if (replyingMessage != null) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFFF8F9FA)) // 更輕盈的底色 + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = replyingMessage.content, + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + IconButton(onClick = onCancelReply, modifier = Modifier.size(24.dp)) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "取消回覆", + tint = Color.Gray + ) + } + } + } + } + + // 2. 實際的輸入工具列 + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 圖片選擇按鈕 + IconButton(onClick = { imagePickerLauncher.launch("image/*") }) { + Icon( + imageVector = Icons.Default.Photo, + contentDescription = "選擇圖片", + tint = Color.Gray + ) + } + + // 輸入文字框 (改為內嵌樣式,去掉自帶的邊框,讓整體更有一體感) + BasicTextField( + value = textState, + onValueChange = { textState = it }, + modifier = Modifier + .weight(1f) + .padding(horizontal = 8.dp), + textStyle = MaterialTheme.typography.bodyLarge, + decorationBox = { innerTextField -> + if (textState.isEmpty()) { + Text("請輸入訊息...", color = Color.LightGray) + } + innerTextField() + } + ) + + // 送出按鈕 + IconButton( + onClick = { + if (textState.isNotBlank() && !isLoading) { + onSendClick(textState) + textState = "" + } + }, + enabled = textState.isNotBlank() && !isLoading + ) { + if (isLoading) { + CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp) + } else { + // 加上小背景讓送出按鈕更有重點 + val iconColor = if (textState.isNotBlank()) Color(0xFFDA7029) else Color.LightGray + Icon( + imageVector = Icons.Default.Send, + contentDescription = "送出", + tint = iconColor + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/ReactionRow.kt b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/ReactionRow.kt new file mode 100644 index 0000000..41a747d --- /dev/null +++ b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/ReactionRow.kt @@ -0,0 +1,60 @@ +package com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.login_v3.data.api.api_class.Reaction +import kotlin.collections.forEach + +@Composable +fun ReactionRow( + reactions: List, + onReactionClick: (String) -> Unit +) { + // 使用 FlowRow,如果貼圖太多會自動折行 + OptIn(ExperimentalLayoutApi::class) + FlowRow( + modifier = Modifier.padding(top = 4.dp, start = 4.dp, end = 4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + reactions.forEach { reaction -> + if (reaction.count > 0) { + Row( + modifier = Modifier + .clip(CircleShape) + .background( + // 如果我自己有按讚,背景變稍微深色/藍色點綴,否則為淡灰色 + if (reaction.meReacted) Color(0xFFBBDEFB) else Color(0xFFEEEEEE) + ) + .clickable { onReactionClick(reaction.emoji) } + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text(text = reaction.emoji, fontSize = 14.sp) + Text( + text = reaction.count.toString(), + style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold), + color = if (reaction.meReacted) Color(0xFF1976D2) else Color.DarkGray + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/UserAvatar.kt b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/UserAvatar.kt new file mode 100644 index 0000000..cff6ba0 --- /dev/null +++ b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging_detail/UserAvatar.kt @@ -0,0 +1,26 @@ +package com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail + +import androidx.compose.foundation.background +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import coil.compose.AsyncImage +import com.example.login_v3.R + +@Composable +fun UserAvatar( + avatarUrl: Any?, // ✨ 改成 Any? + modifier: Modifier = Modifier +) { + AsyncImage( + model = avatarUrl ?: R.drawable.avatar_v1, // 如果為 null,就用預設圖 + contentDescription = "用戶頭像", + modifier = modifier + .background(Color.LightGray, shape = CircleShape) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) +} \ No newline at end of file