diff --git a/app/src/main/java/com/example/login_v3/data/di/AppDatabase.kt b/app/src/main/java/com/example/login_v3/data/di/AppDatabase.kt new file mode 100644 index 0000000..e3c95bc --- /dev/null +++ b/app/src/main/java/com/example/login_v3/data/di/AppDatabase.kt @@ -0,0 +1,11 @@ +package com.example.login_v3.data.di + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.example.login_v3.home.Message.ViewModel.Detail.MessageDao +import com.example.login_v3.home.Message.ViewModel.Detail.MessageEntity + +@Database(entities = [MessageEntity::class], version = 1, exportSchema = false) +abstract class AppDatabase : RoomDatabase() { + abstract fun messageDao(): MessageDao +} \ No newline at end of file diff --git a/app/src/main/java/com/example/login_v3/data/di/DatabaseModule.kt b/app/src/main/java/com/example/login_v3/data/di/DatabaseModule.kt new file mode 100644 index 0000000..0e00fbe --- /dev/null +++ b/app/src/main/java/com/example/login_v3/data/di/DatabaseModule.kt @@ -0,0 +1,37 @@ +package com.example.login_v3.data.di + +import android.content.Context +import androidx.room.Room +import com.example.login_v3.home.Message.ViewModel.Detail.MessageDao +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) // 💡 讓整個 App 生命週期都能拿到 +object DatabaseModule { + + @Provides + @Singleton + fun provideAppDatabase( + @ApplicationContext context: Context // 💡 自動注入 ApplicationContext + ): AppDatabase { + return Room.databaseBuilder( + context, + AppDatabase::class.java, + "tecnologia_chat_db" // 資料庫檔案名稱 + ) + // .fallbackToDestructiveMigration() // 💡 提示:未來如果修改 Entity 欄位導致崩潰,可以先取消這行註解來清除舊資料庫 + .build() + } + + @Provides + @Singleton + fun provideMessageDao(database: AppDatabase): MessageDao { + // 💡 告訴 Hilt:當有人要 MessageDao 時,就從 database 裡面把 messageDao() 拿出來提供給他! + return database.messageDao() + } +} \ No newline at end of file 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 abe3e7e..f09ab33 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 @@ -32,6 +32,7 @@ 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.material.icons.filled.Warning import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.ExperimentalMaterial3Api @@ -52,6 +53,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset @@ -75,6 +77,7 @@ import com.example.login_v3.home.Message.UI.Detail.Message_Messaging_detail.Mess 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.MessageStatus import com.example.login_v3.home.Message.ViewModel.Detail.MessagesUiState import com.example.login_v3.home.Message.ViewModel.Detail.SendMessageState import com.example.login_v3.home.Message.ViewModel.UserStatus @@ -422,6 +425,11 @@ fun MessageRow( else -> Color(0xFFECEFF1) // 對方發的:平常的淺灰色 } + // 💡 核心優化:根據樂觀更新狀態決定氣泡透明度 + val bubbleAlpha = when (message.status) { + MessageStatus.SENDING -> 0.6f // ⏳ 發送中:氣泡變半透明(類似 LINE/Telegram 的未送出狀態) + else -> 1.0f // ✅ 成功或失敗:正常亮度 + } Row( modifier = Modifier @@ -443,115 +451,88 @@ fun MessageRow( ) } - Column( - horizontalAlignment = if (isMe) Alignment.End else Alignment.Start + // 💡 核心優化:如果是「我發的」且「發送失敗」,我們要把驚嘆號跟氣泡橫向並排 + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - Box( - modifier = Modifier - .widthIn(max = 280.dp) - // 💡 重點修正:將點擊/長按事件移到這裡,範圍只侷限在氣泡內 - // 注意:剪裁形狀(clip)建議放在點擊事件前或與形狀同步,確保點擊時的水波紋(Indication)不會超出圓角 - .clip( - RoundedCornerShape( - topStart = 12.dp, topEnd = 12.dp, - bottomStart = if (isMe) 12.dp else 0.dp, - bottomEnd = if (isMe) 0.dp else 12.dp - ) - ) - //long press action short press react - .combinedClickable( - onLongClick = { onReplyClick(message) }, - onClick = { onRowClick(message) } - ) - .background(color = bubbleColor) - .padding(horizontal = 12.dp, vertical = 8.dp) - ) { - 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 + // 🛑 如果是我發的訊息,且發送失敗(FAILED),就把紅色驚嘆號顯示在氣泡「左邊」 + if (isMe && message.status == MessageStatus.FAILED) { + Icon( + imageVector = androidx.compose.material.icons.Icons.Default.Warning, // 需確保有 import + contentDescription = "發送失敗", + tint = Color.Red, + modifier = Modifier + .size(20.dp) + .clickable { + // 💡 加分功能:未來可以在這裡綁定「點擊重發」的 viewModel.sendMessage(...) 邏輯 + } + ) + } + + Column( + horizontalAlignment = if (isMe) Alignment.End else Alignment.Start + ) { + Box( + modifier = Modifier + .widthIn(max = 280.dp) + .clip( + RoundedCornerShape( + topStart = 12.dp, topEnd = 12.dp, + bottomStart = if (isMe) 12.dp else 0.dp, + bottomEnd = if (isMe) 0.dp else 12.dp + ) + ) + .combinedClickable( + onLongClick = { onReplyClick(message) }, + onClick = { onRowClick(message) } + ) + .background(color = bubbleColor) + .alpha(bubbleAlpha) // 👈 🌟 這裡套用透明度控制! + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + Column { + // ... 保持你原本的「被回覆訊息內容渲染(message.repliedMessage)」邏輯 ... + + // 訊息主體內容(文字或圖片) + if (isImage) { + Box( + modifier = Modifier + .padding(top = 4.dp) + .widthIn(max = 240.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color.LightGray) + ) { + AsyncImage( + model = finalImageModel, + contentDescription = "聊天圖片", + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxWidth().heightIn(max = 300.dp), + onError = { errorState -> + android.util.Log.e("ChatImageError", "原因: ${errorState.result.throwable}") + } ) } - } - } - - // 2. 原本的訊息主體內容(文字或圖片) - if (isImage) { - Box( - modifier = Modifier - .padding(top = 4.dp) - .widthIn(max = 240.dp) - .clip(RoundedCornerShape(8.dp)) - .background(Color.LightGray) - ) { - AsyncImage( - model = finalImageModel, - contentDescription = "聊天圖片", - contentScale = ContentScale.Fit, - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 300.dp), - // 🛠️ 移除了 unresolved 的 debugPlaceholder - onError = { errorState -> - // 這樣就能在 Logcat 裡過濾 "ChatImageError" 關鍵字,看看到底是哪個網址拼錯了! - android.util.Log.e( - "ChatImageError", - "圖片載入失敗, 網址為: $finalImageModel, 原因: ${errorState.result.throwable}" - ) - } + } else { + Text( + text = rawContent, + style = MaterialTheme.typography.bodyMedium ) } - } else { - Text( - text = rawContent, - style = MaterialTheme.typography.bodyMedium - ) } } + + if (!message.reactions.isNullOrEmpty()) { + ReactionRow( + reactions = message.reactions, + onReactionClick = { emoji -> onReactionClick(message, emoji) } + ) + } } - // 💡 ✨ 新增:如果這條訊息有 Reaction,就顯示在氣泡的正下方 - if (!message.reactions.isNullOrEmpty()) { - ReactionRow( - reactions = message.reactions, - onReactionClick = { emoji -> onReactionClick(message, emoji) } - ) - } + // 🛑 如果是對方發的訊息(雖然對方理論上不會有 FAILED 狀態,安全起見做對稱),或是想放右邊的提示 + // 可以依此類推。通常聊天軟體只會處理自己發送失敗的提示。 } } }