dm-96
This commit is contained in:
parent
4b8bfe0128
commit
9cda67660b
2 changed files with 57 additions and 36 deletions
|
|
@ -141,17 +141,14 @@ fun MessageMessaging(
|
||||||
onDispose { bottomBarViewModel.setVisible(true) }
|
onDispose { bottomBarViewModel.setVisible(true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎯 修正後的發送狀態監聽:只管提示與重置,絕不插手列表刷新!
|
|
||||||
LaunchedEffect(sendStatus) {
|
LaunchedEffect(sendStatus) {
|
||||||
when (sendStatus) {
|
when (sendStatus) {
|
||||||
is SendMessageState.Error -> {
|
is SendMessageState.Error -> {
|
||||||
val errorMsg = (sendStatus as SendMessageState.Error).message
|
Toast.makeText(context, "發送失敗: ...", Toast.LENGTH_SHORT).show()
|
||||||
Toast.makeText(context, "發送失敗: $errorMsg", Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.resetSendMessageState()
|
viewModel.resetSendMessageState()
|
||||||
}
|
}
|
||||||
is SendMessageState.Success -> {
|
is SendMessageState.Success -> {
|
||||||
// ✅ 樂觀更新成功時,Room 內部已經自己把狀態改成 SUCCESS 並藉由 Flow 推送了
|
// 收到 Success() 訊號,輸入框清空文字,回歸正常可用狀態
|
||||||
// 這裡「完全不需要」再呼叫 viewModel.loadMessages(roomId)!
|
|
||||||
viewModel.resetSendMessageState()
|
viewModel.resetSendMessageState()
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
|
|
@ -426,10 +423,26 @@ fun MessageRow(
|
||||||
|
|
||||||
// 💡 核心優化:根據樂觀更新狀態決定氣泡透明度
|
// 💡 核心優化:根據樂觀更新狀態決定氣泡透明度
|
||||||
val bubbleAlpha = when (message.status) {
|
val bubbleAlpha = when (message.status) {
|
||||||
MessageStatus.SENDING -> 0.6f // ⏳ 發送中:氣泡變半透明(類似 LINE/Telegram 的未送出狀態)
|
MessageStatus.SENDING -> 0.9f // ⏳ 發送中:氣泡變半透明(類似 LINE/Telegram 的未送出狀態)
|
||||||
else -> 1.0f // ✅ 成功或失敗:正常亮度
|
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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -487,8 +500,7 @@ fun MessageRow(
|
||||||
onLongClick = { onReplyClick(message) },
|
onLongClick = { onReplyClick(message) },
|
||||||
onClick = { onRowClick(message) }
|
onClick = { onRowClick(message) }
|
||||||
)
|
)
|
||||||
.background(color = bubbleColor)
|
.background(color = finalBubbleColor)
|
||||||
.alpha(bubbleAlpha) // 👈 🌟 這裡套用透明度控制!
|
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,12 @@ sealed class ReactionUsersState {
|
||||||
data class Error(val message: String) : ReactionUsersState() // 取得失敗
|
data class Error(val message: String) : ReactionUsersState() // 取得失敗
|
||||||
}
|
}
|
||||||
|
|
||||||
//message sending state
|
// 🎯 修正後的發送狀態管理
|
||||||
sealed class SendMessageState {
|
sealed interface SendMessageState {
|
||||||
object Idle : SendMessageState() // 閒置狀態(預設)
|
object Idle : SendMessageState
|
||||||
object Loading : SendMessageState() // 發送中
|
object Loading : SendMessageState
|
||||||
data class Success(val message: Message) : SendMessageState() // 發送成功
|
object Success : SendMessageState // 👈 🌟 這裡改成 object
|
||||||
data class Error(val message: String) : SendMessageState() // 發送失敗
|
data class Error(val message: String) : SendMessageState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -94,11 +94,13 @@ class ChatViewModel @Inject constructor(
|
||||||
// 💡 核心改造一:持續監聽本地資料庫的 Flow
|
// 💡 核心改造一:持續監聽本地資料庫的 Flow
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
fun loadMessages(roomId: String) {
|
fun loadMessages(roomId: String) {
|
||||||
// 先取消上一次的監聽(如果有切換房間的話)
|
|
||||||
messageListenerJob?.cancel()
|
messageListenerJob?.cancel()
|
||||||
|
|
||||||
messageListenerJob = viewModelScope.launch {
|
messageListenerJob = viewModelScope.launch {
|
||||||
|
// 🌟 優化防閃爍:只有在原本不是 Success 的情況下,才去 show 轉圈圈
|
||||||
|
if (_uiState.value !is MessagesUiState.Success) {
|
||||||
_uiState.value = MessagesUiState.Loading
|
_uiState.value = MessagesUiState.Loading
|
||||||
|
}
|
||||||
|
|
||||||
val currentLoggedInUserId = tokenManager.currentUserId.first() ?: ""
|
val currentLoggedInUserId = tokenManager.currentUserId.first() ?: ""
|
||||||
val roomResult = repository.getChatRoom(roomId)
|
val roomResult = repository.getChatRoom(roomId)
|
||||||
|
|
@ -113,29 +115,24 @@ class ChatViewModel @Inject constructor(
|
||||||
Triple("聊天室", UserStatus.UNKNOWN, R.drawable.avatar_v1)
|
Triple("聊天室", UserStatus.UNKNOWN, R.drawable.avatar_v1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🌟 關鍵點:開始收集來自 Room Database 的冷流(Flow)
|
// 持續監聽本地資料庫
|
||||||
repository.getChatMessagesFlow(roomId).collect { localMessages ->
|
repository.getChatMessagesFlow(roomId).collect { localMessages ->
|
||||||
// 只要本地資料庫有任何風吹草動(多一條假訊息、狀態改變、被刪除),這裡都會立刻觸發
|
|
||||||
_uiState.value = MessagesUiState.Success(
|
_uiState.value = MessagesUiState.Success(
|
||||||
roomTitle = title,
|
roomTitle = title,
|
||||||
partnerStatus = status,
|
partnerStatus = status,
|
||||||
partnerAvatarUrl = avatarUrl,
|
partnerAvatarUrl = avatarUrl,
|
||||||
messages = localMessages, // 👈 直接使用來自 Room 的最新訊息列表
|
messages = localMessages,
|
||||||
currentUserId = currentLoggedInUserId
|
currentUserId = currentLoggedInUserId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🌟 背景默默同步:去後端拉取最新訊息寫入 Room
|
// 背景默默跟遠端伺服器同步
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.refreshChatMessages(roomId).onSuccess {
|
repository.refreshChatMessages(roomId).onSuccess {
|
||||||
// 已讀功能保持
|
|
||||||
repository.markAsRead(roomId).onFailure { error ->
|
repository.markAsRead(roomId).onFailure { error ->
|
||||||
Log.e("ChatViewModel", "標記已讀失敗: ${error.message}")
|
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) {
|
fun sendMessage(roomId: String, content: String) {
|
||||||
if (content.isBlank()) return
|
// 1. 安全防護:空白直接攔截
|
||||||
|
if (content.trim().isBlank()) return
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
// 2. 進入 Loading,讓輸入框知道目前在打 API
|
||||||
_sendMessageState.value = SendMessageState.Loading
|
_sendMessageState.value = SendMessageState.Loading
|
||||||
|
|
||||||
|
// 3. 保持你原本非常棒的 token 讀取機制
|
||||||
val replyToId = _replyingMessage.value?.id
|
val replyToId = _replyingMessage.value?.id
|
||||||
val currentLoggedInUserId = tokenManager.currentUserId.first() ?: ""
|
val currentLoggedInUserId = tokenManager.currentUserId.first() ?: ""
|
||||||
|
|
||||||
// 呼叫樂觀更新方法
|
if (currentLoggedInUserId.isBlank()) {
|
||||||
|
_sendMessageState.value = SendMessageState.Error("找不到使用者資訊,請重新登入")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 呼叫樂觀更新方法(此時 SENDING 狀態訊息已秒入 Room,畫面同步劃出半透明氣泡)
|
||||||
val result = repository.sendMessageOptimistically(
|
val result = repository.sendMessageOptimistically(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
currentUserId = currentLoggedInUserId,
|
currentUserId = currentLoggedInUserId,
|
||||||
|
|
@ -160,16 +165,15 @@ class ChatViewModel @Inject constructor(
|
||||||
replyToId = replyToId
|
replyToId = replyToId
|
||||||
)
|
)
|
||||||
|
|
||||||
// 清空回覆狀態
|
// 5. 順手清空回覆狀態
|
||||||
_replyingMessage.value = null
|
_replyingMessage.value = null
|
||||||
|
|
||||||
result.onSuccess {
|
result.onSuccess {
|
||||||
// 🌟 發送成功!此時 Room 內對應的臨時訊息已經被改為 SUCCESS 了
|
// 🌟 完美:因為變成了 object,直接指派名字即可,絕不會再噴 Too many arguments!
|
||||||
// Flow 會自動讓 UI 刷新,ViewModel 這裡「不需要」手動做 updatedMessages + newMessage
|
_sendMessageState.value = SendMessageState.Success
|
||||||
_sendMessageState.value = SendMessageState.Idle
|
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
Log.e("ChatViewModel", "Send message failed", error)
|
Log.e("ChatViewModel", "Send message failed", error)
|
||||||
// 🌟 發送失敗!Room 內的臨時訊息已被改為 FAILED,畫面上會顯示驚嘆號
|
|
||||||
_sendMessageState.value = SendMessageState.Error(error.message ?: "未知錯誤")
|
_sendMessageState.value = SendMessageState.Error(error.message ?: "未知錯誤")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -202,11 +206,16 @@ class ChatViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_sendMessageState.value = SendMessageState.Loading
|
_sendMessageState.value = SendMessageState.Loading
|
||||||
val result = repository.uploadAttachment(roomId, fileUri)
|
val result = repository.uploadAttachment(roomId, fileUri)
|
||||||
result.onSuccess { newMessage ->
|
|
||||||
_sendMessageState.value = SendMessageState.Success(newMessage)
|
result.onSuccess {
|
||||||
// 注意:如果上傳檔案未來也想做樂觀更新,可以用跟 sendMessage 相同的邏輯改造 Repository。
|
// 💡 ✅ 修正點:移除參數括號,直接指派 object 狀態
|
||||||
// 如果目前保持原樣,因為原本的 loadMessages 是 Flow,它會持續監聽,只要 refreshChatMessages 被觸發或後端有推播,它就會出現。
|
_sendMessageState.value = SendMessageState.Success
|
||||||
// 為了保險起見,若 uploadAttachment 還沒改造,可以先留著手動塞入的邏輯,或者直接在成功後呼叫 repository.refreshChatMessages(roomId)
|
|
||||||
|
// 🌟 核心同步:因為上傳檔案成功後,後端可能已經將新訊息寫入。
|
||||||
|
// 我們在背景主動觸發一次 refresh 默默拉回最新訊息寫入 Room,
|
||||||
|
// 這樣你的 MessageList 就能透過 Flow 秒速自動跳出剛剛上傳的圖片氣泡!
|
||||||
|
repository.refreshChatMessages(roomId)
|
||||||
|
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
Log.e("ChatViewModel", "Upload attachment failed", error)
|
Log.e("ChatViewModel", "Upload attachment failed", error)
|
||||||
_sendMessageState.value = SendMessageState.Error(error.message ?: "上傳失敗")
|
_sendMessageState.value = SendMessageState.Error(error.message ?: "上傳失敗")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue