This commit is contained in:
Eric85710 2026-06-07 06:07:19 +08:00
parent 4b8bfe0128
commit 9cda67660b
2 changed files with 57 additions and 36 deletions

View file

@ -141,17 +141,14 @@ fun MessageMessaging(
onDispose { bottomBarViewModel.setVisible(true) }
}
// 🎯 修正後的發送狀態監聽:只管提示與重置,絕不插手列表刷新!
LaunchedEffect(sendStatus) {
when (sendStatus) {
is SendMessageState.Error -> {
val errorMsg = (sendStatus as SendMessageState.Error).message
Toast.makeText(context, "發送失敗: $errorMsg", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "發送失敗: ...", Toast.LENGTH_SHORT).show()
viewModel.resetSendMessageState()
}
is SendMessageState.Success -> {
// ✅ 樂觀更新成功時,Room 內部已經自己把狀態改成 SUCCESS 並藉由 Flow 推送了
// 這裡「完全不需要」再呼叫 viewModel.loadMessages(roomId)!
// 收到 Success() 訊號,輸入框清空文字,回歸正常可用狀態
viewModel.resetSendMessageState()
}
else -> {}
@ -426,10 +423,26 @@ fun MessageRow(
// 💡 核心優化:根據樂觀更新狀態決定氣泡透明度
val bubbleAlpha = when (message.status) {
MessageStatus.SENDING -> 0.6f // ⏳ 發送中:氣泡變半透明(類似 LINE/Telegram 的未送出狀態)
MessageStatus.SENDING -> 0.9f // ⏳ 發送中:氣泡變半透明(類似 LINE/Telegram 的未送出狀態)
else -> 1.0f // ✅ 成功或失敗:正常亮度
}
val finalBubbleColor = remember(message.status, isHighlight) {
val baseColor = when {
isMe && isHighlight -> Color(0xFFB4E197)
isMe -> Color(0xFFDCF8C6)
!isMe && isHighlight -> Color(0xFFCFD8DC)
else -> Color(0xFFECEFF1)
}
// ⏳ 如果是發送中,直接讓顏色本身帶 alpha(例如 0.75f,防止被背景吃掉)
if (message.status == MessageStatus.SENDING) {
baseColor.copy(alpha = 0.75f)
} else {
baseColor // 成功或失敗時維持實色
}
}
Row(
modifier = Modifier
.fillMaxWidth()
@ -487,8 +500,7 @@ fun MessageRow(
onLongClick = { onReplyClick(message) },
onClick = { onRowClick(message) }
)
.background(color = bubbleColor)
.alpha(bubbleAlpha) // 👈 🌟 這裡套用透明度控制!
.background(color = finalBubbleColor)
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Column {

View file

@ -28,12 +28,12 @@ sealed class ReactionUsersState {
data class Error(val message: String) : ReactionUsersState() // 取得失敗
}
//message sending state
sealed class SendMessageState {
object Idle : SendMessageState() // 閒置狀態(預設)
object Loading : SendMessageState() // 發送中
data class Success(val message: Message) : SendMessageState() // 發送成功
data class Error(val message: String) : SendMessageState() // 發送失敗
// 🎯 修正後的發送狀態管理
sealed interface SendMessageState {
object Idle : SendMessageState
object Loading : SendMessageState
object Success : SendMessageState // 👈 🌟 這裡改成 object
data class Error(val message: String) : SendMessageState
}
@ -94,11 +94,13 @@ class ChatViewModel @Inject constructor(
// 💡 核心改造一:持續監聽本地資料庫的 Flow
// =====================================================================
fun loadMessages(roomId: String) {
// 先取消上一次的監聽(如果有切換房間的話)
messageListenerJob?.cancel()
messageListenerJob = viewModelScope.launch {
_uiState.value = MessagesUiState.Loading
// 🌟 優化防閃爍:只有在原本不是 Success 的情況下,才去 show 轉圈圈
if (_uiState.value !is MessagesUiState.Success) {
_uiState.value = MessagesUiState.Loading
}
val currentLoggedInUserId = tokenManager.currentUserId.first() ?: ""
val roomResult = repository.getChatRoom(roomId)
@ -113,29 +115,24 @@ class ChatViewModel @Inject constructor(
Triple("聊天室", UserStatus.UNKNOWN, R.drawable.avatar_v1)
}
// 🌟 關鍵點:開始收集來自 Room Database 的冷流(Flow)
// 持續監聽本地資料庫
repository.getChatMessagesFlow(roomId).collect { localMessages ->
// 只要本地資料庫有任何風吹草動(多一條假訊息、狀態改變、被刪除),這裡都會立刻觸發
_uiState.value = MessagesUiState.Success(
roomTitle = title,
partnerStatus = status,
partnerAvatarUrl = avatarUrl,
messages = localMessages, // 👈 直接使用來自 Room 的最新訊息列表
messages = localMessages,
currentUserId = currentLoggedInUserId
)
}
}
// 🌟 背景默默同步:去後端拉取最新訊息寫入 Room
// 背景默默跟遠端伺服器同步
viewModelScope.launch {
repository.refreshChatMessages(roomId).onSuccess {
// 已讀功能保持
repository.markAsRead(roomId).onFailure { error ->
Log.e("ChatViewModel", "標記已讀失敗: ${error.message}")
}
}.onFailure { error ->
Log.e("ChatViewModel", "背景同步失敗,改用純離線快取", error)
// 這裡不用特地切換到 Error 狀態,因為 getChatMessagesFlow 可能已經把離線快取顯示在畫面上了
}
}
}
@ -144,15 +141,23 @@ class ChatViewModel @Inject constructor(
// 💡 核心改造二:發送文字訊息(樂觀更新)
// =====================================================================
fun sendMessage(roomId: String, content: String) {
if (content.isBlank()) return
// 1. 安全防護:空白直接攔截
if (content.trim().isBlank()) return
viewModelScope.launch {
// 2. 進入 Loading,讓輸入框知道目前在打 API
_sendMessageState.value = SendMessageState.Loading
// 3. 保持你原本非常棒的 token 讀取機制
val replyToId = _replyingMessage.value?.id
val currentLoggedInUserId = tokenManager.currentUserId.first() ?: ""
// 呼叫樂觀更新方法
if (currentLoggedInUserId.isBlank()) {
_sendMessageState.value = SendMessageState.Error("找不到使用者資訊,請重新登入")
return@launch
}
// 4. 呼叫樂觀更新方法(此時 SENDING 狀態訊息已秒入 Room,畫面同步劃出半透明氣泡)
val result = repository.sendMessageOptimistically(
roomId = roomId,
currentUserId = currentLoggedInUserId,
@ -160,16 +165,15 @@ class ChatViewModel @Inject constructor(
replyToId = replyToId
)
// 清空回覆狀態
// 5. 順手清空回覆狀態
_replyingMessage.value = null
result.onSuccess {
// 🌟 發送成功!此時 Room 內對應的臨時訊息已經被改為 SUCCESS 了
// Flow 會自動讓 UI 刷新,ViewModel 這裡「不需要」手動做 updatedMessages + newMessage
_sendMessageState.value = SendMessageState.Idle
// 🌟 完美:因為變成了 object,直接指派名字即可,絕不會再噴 Too many arguments!
_sendMessageState.value = SendMessageState.Success
}.onFailure { error ->
Log.e("ChatViewModel", "Send message failed", error)
// 🌟 發送失敗!Room 內的臨時訊息已被改為 FAILED,畫面上會顯示驚嘆號
_sendMessageState.value = SendMessageState.Error(error.message ?: "未知錯誤")
}
}
@ -202,11 +206,16 @@ class ChatViewModel @Inject constructor(
viewModelScope.launch {
_sendMessageState.value = SendMessageState.Loading
val result = repository.uploadAttachment(roomId, fileUri)
result.onSuccess { newMessage ->
_sendMessageState.value = SendMessageState.Success(newMessage)
// 注意:如果上傳檔案未來也想做樂觀更新,可以用跟 sendMessage 相同的邏輯改造 Repository。
// 如果目前保持原樣,因為原本的 loadMessages 是 Flow,它會持續監聽,只要 refreshChatMessages 被觸發或後端有推播,它就會出現。
// 為了保險起見,若 uploadAttachment 還沒改造,可以先留著手動塞入的邏輯,或者直接在成功後呼叫 repository.refreshChatMessages(roomId)
result.onSuccess {
// 💡 ✅ 修正點:移除參數括號,直接指派 object 狀態
_sendMessageState.value = SendMessageState.Success
// 🌟 核心同步:因為上傳檔案成功後,後端可能已經將新訊息寫入。
// 我們在背景主動觸發一次 refresh 默默拉回最新訊息寫入 Room,
// 這樣你的 MessageList 就能透過 Flow 秒速自動跳出剛剛上傳的圖片氣泡!
repository.refreshChatMessages(roomId)
}.onFailure { error ->
Log.e("ChatViewModel", "Upload attachment failed", error)
_sendMessageState.value = SendMessageState.Error(error.message ?: "上傳失敗")