dm-49
This commit is contained in:
parent
f8799511de
commit
2ea1890a0d
2 changed files with 246 additions and 79 deletions
|
|
@ -13,6 +13,8 @@ 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
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
|
@ -35,12 +37,14 @@ import androidx.compose.material.icons.filled.Add
|
||||||
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.Close
|
||||||
|
import androidx.compose.material.icons.filled.Photo
|
||||||
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
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
|
@ -48,6 +52,7 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
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
|
||||||
|
|
@ -67,14 +72,17 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import coil.compose.AsyncImage
|
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.data.api.api_class.Reaction
|
||||||
import com.example.login_v3.home.Message.ViewModel.Detail.ChatViewModel
|
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.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.Detail.SendMessageState
|
||||||
import com.example.login_v3.home.Message.ViewModel.UserStatus
|
import com.example.login_v3.home.Message.ViewModel.UserStatus
|
||||||
import com.example.login_v3.navigation.BottomBarViewModel
|
import com.example.login_v3.navigation.BottomBarViewModel
|
||||||
|
|
@ -99,6 +107,17 @@ fun MessageMessaging(
|
||||||
//reply function
|
//reply function
|
||||||
val replyingMessage by viewModel.replyingMessage.collectAsStateWithLifecycle()
|
val replyingMessage by viewModel.replyingMessage.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
// 💡 1. 取得 ViewModel 裡的 Reaction 狀態
|
||||||
|
val reactionState by viewModel.reactionUsersState.collectAsStateWithLifecycle()
|
||||||
|
// 控制 BottomSheet 顯示的變數
|
||||||
|
var showReactionBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// 當狀態變成 Success 且不是空名單時,或者正在 Loading,就開啟 BottomSheet
|
||||||
|
LaunchedEffect(reactionState) {
|
||||||
|
if (reactionState is ReactionUsersState.Success || reactionState is ReactionUsersState.Loading) {
|
||||||
|
showReactionBottomSheet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
//進入時重整
|
//進入時重整
|
||||||
LaunchedEffect(roomId) {
|
LaunchedEffect(roomId) {
|
||||||
viewModel.loadMessages(roomId)
|
viewModel.loadMessages(roomId)
|
||||||
|
|
@ -132,6 +151,69 @@ fun MessageMessaging(
|
||||||
is MessagesUiState.Loading -> "載入中..."
|
is MessagesUiState.Loading -> "載入中..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 💡 2. 渲染 BottomSheet 視窗
|
||||||
|
if (showReactionBottomSheet) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = {
|
||||||
|
showReactionBottomSheet = false
|
||||||
|
viewModel.resetReactionUsersState() // 關閉時記得清空狀態,避免下次閃出舊資料
|
||||||
|
},
|
||||||
|
sheetState = rememberModalBottomSheetState()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 32.dp, start = 16.dp, end = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "按讚的名單",
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
when (val state = reactionState) {
|
||||||
|
is ReactionUsersState.Loading -> {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
|
||||||
|
}
|
||||||
|
is ReactionUsersState.Success -> {
|
||||||
|
if (state.users.isEmpty()) {
|
||||||
|
Text("目前沒有人點擊", color = Color.Gray)
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
items(state.users) { userName ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
// 這裡可以用一個預設頭像,或是單純顯示文字
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.AccountCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.Gray,
|
||||||
|
modifier = Modifier.size(36.dp).padding(end = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = userName,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ReactionUsersState.Error -> {
|
||||||
|
Text(text = state.message, color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -216,7 +298,14 @@ fun MessageMessaging(
|
||||||
messages = state.messages,
|
messages = state.messages,
|
||||||
partnerAvatarUrl = state.partnerAvatarUrl,
|
partnerAvatarUrl = state.partnerAvatarUrl,
|
||||||
partnerDisplayName = state.roomTitle,
|
partnerDisplayName = state.roomTitle,
|
||||||
onReplyClick = { message -> viewModel.setReplyingMessage(message) }
|
onReplyClick = { message -> viewModel.setReplyingMessage(message) },
|
||||||
|
onReactionClick = { message, emoji ->
|
||||||
|
viewModel.loadMessageReactionUsers(
|
||||||
|
roomId = roomId,
|
||||||
|
messageId = message.id,
|
||||||
|
emoji = emoji
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -238,6 +327,7 @@ fun MessageList(
|
||||||
partnerDisplayName: String,
|
partnerDisplayName: String,
|
||||||
messages: List<Message>,
|
messages: List<Message>,
|
||||||
onReplyClick: (Message) -> Unit,
|
onReplyClick: (Message) -> Unit,
|
||||||
|
onReactionClick: (Message, String) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|
@ -254,7 +344,8 @@ fun MessageList(
|
||||||
partnerDisplayName = partnerDisplayName,
|
partnerDisplayName = partnerDisplayName,
|
||||||
isMe = isMe,
|
isMe = isMe,
|
||||||
currentUserId = currentUserId,
|
currentUserId = currentUserId,
|
||||||
onReplyClick = onReplyClick
|
onReplyClick = onReplyClick,
|
||||||
|
onReactionClick = onReactionClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -267,7 +358,8 @@ fun MessageRow(
|
||||||
partnerAvatarUrl: Any?,
|
partnerAvatarUrl: Any?,
|
||||||
partnerDisplayName: String,
|
partnerDisplayName: String,
|
||||||
currentUserId: String,
|
currentUserId: String,
|
||||||
onReplyClick: (Message) -> Unit // 💡 新增參數
|
onReplyClick: (Message) -> Unit,
|
||||||
|
onReactionClick: (Message, String) -> 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()
|
||||||
|
|
@ -295,6 +387,9 @@ fun MessageRow(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = if (isMe) Alignment.End else Alignment.Start
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.widthIn(max = 280.dp)
|
.widthIn(max = 280.dp)
|
||||||
|
|
@ -378,6 +473,15 @@ fun MessageRow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 💡 ✨ 新增:如果這條訊息有 Reaction,就顯示在氣泡的正下方
|
||||||
|
if (!message.reactions.isNullOrEmpty()) {
|
||||||
|
ReactionRow(
|
||||||
|
reactions = message.reactions,
|
||||||
|
onReactionClick = { emoji -> onReactionClick(message, emoji) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -392,6 +496,16 @@ fun MessageInputBar(
|
||||||
) {
|
) {
|
||||||
var textState by remember { mutableStateOf("") }
|
var textState by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
val imagePickerLauncher =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.GetContent()
|
||||||
|
) { uri: Uri? ->
|
||||||
|
|
||||||
|
uri?.let {
|
||||||
|
onImageSelected(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 💡 使用打包器,確保軟鍵盤彈出時系統會正確計算高度
|
// 💡 使用打包器,確保軟鍵盤彈出時系統會正確計算高度
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|
@ -444,8 +558,17 @@ fun MessageInputBar(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 圖片選擇按鈕 (範例,可換成你的實作)
|
// 圖片選擇按鈕 (範例,可換成你的實作)
|
||||||
IconButton(onClick = { /* 觸發你的圖片選取器,回傳 URI */ }) {
|
// ✅ 圖片按鈕
|
||||||
Icon(imageVector = Icons.Default.Add, contentDescription = "選擇圖片", tint = Color.Gray)
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
imagePickerLauncher.launch("image/*")
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Photo,
|
||||||
|
contentDescription = "選擇圖片",
|
||||||
|
tint = Color.Gray
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 輸入文字框
|
// 輸入文字框
|
||||||
|
|
@ -496,3 +619,42 @@ fun UserAvatar(
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReactionRow(
|
||||||
|
reactions: List<Reaction>,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -215,6 +215,11 @@ class ChatViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 💡 請確保這段程式碼有確實待在 ChatViewModel 裡面
|
||||||
|
fun resetReactionUsersState() {
|
||||||
|
_reactionUsersState.value = ReactionUsersState.Idle
|
||||||
|
}
|
||||||
|
|
||||||
//新增:重設發送狀態的 function
|
//新增:重設發送狀態的 function
|
||||||
fun resetSendMessageState() {
|
fun resetSendMessageState() {
|
||||||
_sendMessageState.value = SendMessageState.Idle
|
_sendMessageState.value = SendMessageState.Idle
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue