This commit is contained in:
Eric85710 2026-06-07 05:39:18 +08:00
parent ab10357042
commit bf006e6c7e
3 changed files with 128 additions and 99 deletions

View file

@ -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
}

View file

@ -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()
}
}

View file

@ -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 狀態,安全起見做對稱),或是想放右邊的提示
// 可以依此類推。通常聊天軟體只會處理自己發送失敗的提示。
}
}
}