From ab10357042aada3605821f4b999095891efc1361 Mon Sep 17 00:00:00 2001 From: Eric85710 Date: Sun, 7 Jun 2026 05:30:24 +0800 Subject: [PATCH] dm-93 --- .../login_v3/data/api/api_class/DM_Basic.kt | 10 +- .../data/repository/dm/DM_Basic_Repository.kt | 132 ++++++-- .../Message/ViewModel/Detail/Message_Cache.kt | 18 +- .../Detail/Message_Messaging_ViewModel.kt | 302 +++++++----------- 4 files changed, 242 insertions(+), 220 deletions(-) diff --git a/app/src/main/java/com/example/login_v3/data/api/api_class/DM_Basic.kt b/app/src/main/java/com/example/login_v3/data/api/api_class/DM_Basic.kt index 2af46f3..aff99ce 100644 --- a/app/src/main/java/com/example/login_v3/data/api/api_class/DM_Basic.kt +++ b/app/src/main/java/com/example/login_v3/data/api/api_class/DM_Basic.kt @@ -1,6 +1,7 @@ package com.example.login_v3.data.api.api_class import com.example.login_v3.R +import com.example.login_v3.home.Message.ViewModel.Detail.MessageStatus import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -103,11 +104,14 @@ data class Message( @Json(name = "reply_to_id") val replyToId: String? = null, @kotlin.jvm.Transient var repliedMessage: Message? = null, - @Json(name = "forwarded_from_id") val forwardedFromId: String? = null, @Json(name = "forwarded_from_room_id") val forwardedFromRoomId: String? = null, - @Json(name = "attachment") val attachment: Attachment? = null, // 直接使用外層的 Attachment - @Json(name = "reactions") val reactions: List? = emptyList() // 直接使用外層的 Reaction + @Json(name = "attachment") val attachment: Attachment? = null, + @Json(name = "reactions") val reactions: List? = emptyList(), + + // 🌟 核心新增:讓 UI 可以辨識訊息發送狀態! + @kotlin.jvm.Transient // 👈 告訴 Moshi 這個欄位不需要解析 JSON,這是我們本地維護的 + val status: MessageStatus = MessageStatus.SUCCESS ) //send message diff --git a/app/src/main/java/com/example/login_v3/data/repository/dm/DM_Basic_Repository.kt b/app/src/main/java/com/example/login_v3/data/repository/dm/DM_Basic_Repository.kt index d86940c..b783cc5 100644 --- a/app/src/main/java/com/example/login_v3/data/repository/dm/DM_Basic_Repository.kt +++ b/app/src/main/java/com/example/login_v3/data/repository/dm/DM_Basic_Repository.kt @@ -13,11 +13,16 @@ import com.example.login_v3.data.api.api_class.MessageResponse import com.example.login_v3.data.api.api_class.RoomListResponse import com.example.login_v3.data.api.api_class.SendMessageRequest import com.example.login_v3.home.Message.ViewModel.Detail.MessageDao +import com.example.login_v3.home.Message.ViewModel.Detail.MessageEntity +import com.example.login_v3.home.Message.ViewModel.Detail.MessageStatus import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.toRequestBody import java.io.ByteArrayOutputStream +import java.util.UUID import javax.inject.Inject import javax.inject.Singleton @@ -43,33 +48,122 @@ class ChatRoomsRepository @Inject constructor( } } - suspend fun getChatMessages(roomId: String): Result { + fun getChatMessagesFlow(roomId: String): Flow> { + return messageDao.getMessagesFlow(roomId).map { entities -> + val domainMessages = entities.map { entity -> + Message( + id = entity.id, + chatRoomId = entity.chatRoomId, // 👈 完美對齊 + senderId = entity.senderId, + content = entity.content, + type = entity.type, + createdAt = entity.createdAt, // 👈 String 對 String + isEdited = entity.isEdited, + isDeleted = entity.isDeleted, + replyToId = entity.replyToId, + status = entity.status // 👈 把 Room 儲存的狀態倒出來給 UI + ) + } + + // 你原本的處理回覆邏輯 + val messageMap = domainMessages.associateBy { it.id } + domainMessages.forEach { message -> + if (!message.replyToId.isNullOrBlank()) { + message.repliedMessage = messageMap[message.replyToId] + } + } + domainMessages + } + } + + // api get data and put to local + suspend fun refreshChatMessages(roomId: String): Result { return try { val response = api.getChatMessages(roomId) + if (response.isSuccessful) { + val body = response.body() + if (body != null) { + body.messages.forEach { networkMessage -> + val entity = MessageEntity( + id = networkMessage.id, + chatRoomId = roomId, + senderId = networkMessage.senderId, + content = networkMessage.content, + type = networkMessage.type, + createdAt = networkMessage.createdAt, + isEdited = networkMessage.isEdited, + isDeleted = networkMessage.isDeleted, + replyToId = networkMessage.replyToId, + status = MessageStatus.SUCCESS // 後端來的都是真的發送成功的 + ) + messageDao.insertOrUpdate(entity) + } + Result.success(Unit) + } else { Result.failure(Exception("Empty body")) } + } else { Result.failure(Exception("Error code: ${response.code()}")) } + } catch (e: Exception) { Result.failure(e) } + } + + //fake sending + suspend fun sendMessageOptimistically( + roomId: String, + currentUserId: String, + content: String, + replyToId: String? = null + ): Result { + val tempId = UUID.randomUUID().toString() + // 💡 配合你的 String 格式,產生 ISO 時間字串或乾淨的目前時間字串,例如 2026-06-07T05:26:48Z + val currentIsoTime = java.time.Instant.now().toString() + + val tempMessageEntity = MessageEntity( + id = tempId, + chatRoomId = roomId, + senderId = currentUserId, + content = content, + type = "text", // 預設普通文字 + createdAt = currentIsoTime, + isEdited = false, + isDeleted = false, + replyToId = replyToId, + status = MessageStatus.SENDING // 👈 畫面馬上呈現半透明淡灰色! + ) + + messageDao.insertOrUpdate(tempMessageEntity) + + return try { + val requestBody = SendMessageRequest(content = content, replyToId = replyToId) + val response = api.sendMessage(roomId, requestBody) if (response.isSuccessful) { val body = response.body() if (body != null) { + // 成功:刪除假的,插入真的 + messageDao.deleteById(tempId) - // 💡 核心邏輯:將訊息列表轉成 Map,方便用 ID 快速查找 - val messageMap = body.messages.associateBy { it.id } - - // 走訪每一條訊息,如果它有 replyToId,就去 Map 裡面找出那一條訊息塞給它 - body.messages.forEach { message -> - if (!message.replyToId.isNullOrBlank()) { - message.repliedMessage = messageMap[message.replyToId] - } - } - - Result.success(body) + val successEntity = MessageEntity( + id = body.id, + chatRoomId = roomId, + senderId = body.senderId, + content = body.content, + type = body.type, + createdAt = body.createdAt, // 後端確認的正式時間 + isEdited = body.isEdited, + isDeleted = body.isDeleted, + replyToId = body.replyToId, + status = MessageStatus.SUCCESS + ) + messageDao.insertOrUpdate(successEntity) + Result.success(Unit) } else { - Result.failure(Exception("回應身體為空 (Empty response body)")) + messageDao.insertOrUpdate(tempMessageEntity.copy(status = MessageStatus.FAILED)) + Result.failure(Exception("Body null")) } } else { - val errorMsg = response.errorBody()?.string() ?: "未知錯誤" - Result.failure(Exception("網路請求失敗,錯誤碼: ${response.code()}, 訊息: $errorMsg")) + messageDao.insertOrUpdate(tempMessageEntity.copy(status = MessageStatus.FAILED)) + Result.failure(Exception("API Error")) } } catch (e: Exception) { + messageDao.insertOrUpdate(tempMessageEntity.copy(status = MessageStatus.FAILED)) Result.failure(e) } } @@ -296,16 +390,14 @@ class ChatRoomsRepository @Inject constructor( suspend fun deleteMessage(roomId: String, messageId: String): Result { return try { val response = api.deleteMessage(roomId, messageId) - if (response.isSuccessful) { - // 後端回傳 204 No Content 成功時沒有 body,直接回傳 Unit 即可 + // 💡 既然刪除了,本地也一併砍掉,UI 就會立刻同步消失 + messageDao.deleteById(messageId) Result.success(Unit) } else { - val errorMsg = response.errorBody()?.string() ?: "未知錯誤" - Result.failure(Exception("刪除訊息失敗,錯誤碼: ${response.code()}, 訊息: $errorMsg")) + Result.failure(Exception("Delete failed")) } } catch (e: Exception) { - // 捕捉網路斷線等異常狀況 Result.failure(e) } } diff --git a/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Message_Cache.kt b/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Message_Cache.kt index 204680e..5ad2de6 100644 --- a/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Message_Cache.kt +++ b/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Message_Cache.kt @@ -12,18 +12,22 @@ enum class MessageStatus { SENDING, SUCCESS, FAILED } @Entity(tableName = "messages") data class MessageEntity( - @PrimaryKey val id: String, // 臨時訊息可以用 UUID.randomUUID().toString() - val roomId: String, - val content: String, + @PrimaryKey val id: String, + val chatRoomId: String, // 👈 對齊你的 @Json(name = "chat_room_id") val senderId: String, - val timestamp: Long, - val status: MessageStatus // 👈 核心:用來控制 UI 顯示樣式 + val content: String, + val type: String, // 👈 新增:文字、圖片等類型 + val createdAt: String, // 👈 核心修正:改成與 API 一致的 String 格式 + val isEdited: Boolean, // 👈 新增 + val isDeleted: Boolean, // 👈 新增 + val replyToId: String?, // 👈 新增 + val status: MessageStatus // 👈 樂觀更新狀態控制 (SENDING, SUCCESS, FAILED) ) @Dao interface MessageDao { - // 💡 監聽特定聊天室的訊息流 - @Query("SELECT * FROM messages WHERE roomId = :roomId ORDER BY timestamp ASC") + // 💡 修正這裡:WHERE 條件欄位改為 chatRoomId + @Query("SELECT * FROM messages WHERE chatRoomId = :roomId ORDER BY createdAt ASC") fun getMessagesFlow(roomId: String): Flow> @Insert(onConflict = OnConflictStrategy.REPLACE) diff --git a/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Message_Messaging_ViewModel.kt b/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Message_Messaging_ViewModel.kt index f8123be..41291bf 100644 --- a/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Message_Messaging_ViewModel.kt +++ b/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Message_Messaging_ViewModel.kt @@ -14,6 +14,7 @@ import com.example.login_v3.data.api.api_class.Message import com.example.login_v3.data.api.api_class.fullContactAvatarUrl import com.example.login_v3.data.repository.basic.TokenManager import com.example.login_v3.home.Message.ViewModel.UserStatus +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -64,220 +65,119 @@ class ChatViewModel @Inject constructor( private val tokenManager: TokenManager ) : ViewModel() { - // --- 訊息長按動作選單狀態 (Action Menu State) --- - - // 1. 私有的 MutableStateFlow,用來控制長按選中的訊息 + // --- 訊息長按動作選單與回覆狀態 (保持原樣) --- + // 💡 補上這兩行宣告 + private val _deleteMessageState = MutableStateFlow(DeleteMessageState.Idle) + val deleteMessageState: StateFlow = _deleteMessageState.asStateFlow() private val _actionMessage = MutableStateFlow(null) - - // 2. 公開給 Compose UI 觀察的唯讀 StateFlow val actionMessage: StateFlow = _actionMessage.asStateFlow() - /** - * 設定目前哪一則訊息被長按選取(開啟動作選單) - */ - fun setActionMessage(message: Message?) { - _actionMessage.value = message - } + fun setActionMessage(message: Message?) { _actionMessage.value = message } + fun clearActionMessage() { _actionMessage.value = null } - /** - * 清除目前選取的訊息狀態(關閉動作選單) - */ - fun clearActionMessage() { - _actionMessage.value = null - } - - - //reply function private val _replyingMessage = MutableStateFlow(null) val replyingMessage: StateFlow = _replyingMessage.asStateFlow() - // 💡 新增:當使用者點選某條訊息的「回覆」按鈕時呼叫 - fun setReplyingMessage(message: Message?) { - _replyingMessage.value = message - } + fun setReplyingMessage(message: Message?) { _replyingMessage.value = message } - //UI state var + // --- 核心狀態管理 --- private val _uiState = MutableStateFlow(MessagesUiState.Loading) val uiState: StateFlow = _uiState.asStateFlow() - //message var + // 💡 提示:在做法 A 下,_sendMessageState 通常只用來控制輸入框的「Loading」轉圈圈或置灰,真正的訊息成功/失敗已經內嵌在 Message 本身的 status 裡了。 private val _sendMessageState = MutableStateFlow(SendMessageState.Idle) val sendMessageState: StateFlow = _sendMessageState + // 用來管理監聽 Room Flow 的 Job,避免重複綁定 + private var messageListenerJob: Job? = null + + // ===================================================================== + // 💡 核心改造一:持續監聽本地資料庫的 Flow + // ===================================================================== fun loadMessages(roomId: String) { - viewModelScope.launch { + // 先取消上一次的監聽(如果有切換房間的話) + messageListenerJob?.cancel() + + messageListenerJob = viewModelScope.launch { _uiState.value = MessagesUiState.Loading - - //isMe val currentLoggedInUserId = tokenManager.currentUserId.first() ?: "" - // 1. 同時獲取房間資訊與訊息列表 - val roomResult = repository.getChatRoom(roomId) // ⚠️ 需確保 Repository 有提供此方法 - val messagesResult = repository.getChatMessages(roomId) + val roomResult = repository.getChatRoom(roomId) - if (messagesResult.isSuccess) { - val messagesResponse = messagesResult.getOrNull()!! - - // 2. 根據房間資訊決定標題邏輯 - val room = roomResult.getOrNull() - // ✨ 這裡改用 Any? 來接收擴充屬性的結果 - val (title, status, avatarUrl: Any?) = if (room != null) { - val name = room.partner?.displayName ?: room.partner?.username ?: "未知用戶" - val userStatus = UserStatus.fromString(room.partner?.status) - // 👇 直接呼叫你寫好的擴充屬性 - val avatar = room.partner?.fullContactAvatarUrl - Triple(name, userStatus, avatar) - } else { - // 如果沒有房間資訊,就給預設的頭像資源 - Triple("聊天室", UserStatus.UNKNOWN, R.drawable.avatar_v1) - } + val room = roomResult.getOrNull() + val (title, status, avatarUrl: Any?) = if (room != null) { + val name = room.partner?.displayName ?: room.partner?.username ?: "未知用戶" + val userStatus = UserStatus.fromString(room.partner?.status) + val avatar = room.partner?.fullContactAvatarUrl + Triple(name, userStatus, avatar) + } else { + 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, //這裡就會是完整的網址或 R.drawable.avatar_v1 - messages = messagesResponse.messages, + partnerAvatarUrl = avatarUrl, + messages = localMessages, // 👈 直接使用來自 Room 的最新訊息列表 currentUserId = currentLoggedInUserId ) + } + } - //已讀功能 - launch { - repository.markAsRead(roomId) - .onFailure { error -> - // 這裡通常不需要回報給 UI(不用跳 Error 畫面) - // 只需要印個 Log 知道出事了就好 - Log.e("ChatViewModel", "標記已讀失敗: ${error.message}") - } + // 🌟 背景默默同步:去後端拉取最新訊息寫入 Room + viewModelScope.launch { + repository.refreshChatMessages(roomId).onSuccess { + // 已讀功能保持 + repository.markAsRead(roomId).onFailure { error -> + Log.e("ChatViewModel", "標記已讀失敗: ${error.message}") } - } else { - val exception = messagesResult.exceptionOrNull() - _uiState.value = MessagesUiState.Error(message = exception?.message ?: "未知錯誤") + }.onFailure { error -> + Log.e("ChatViewModel", "背景同步失敗,改用純離線快取", error) + // 這裡不用特地切換到 Error 狀態,因為 getChatMessagesFlow 可能已經把離線快取顯示在畫面上了 } } } - - //upload image - fun sendMessage(roomId: String, content: String, replyToId: String? = null) { + // ===================================================================== + // 💡 核心改造二:發送文字訊息(樂觀更新) + // ===================================================================== + fun sendMessage(roomId: String, content: String) { if (content.isBlank()) return viewModelScope.launch { _sendMessageState.value = SendMessageState.Loading - // 💡 取得當前是否有正在回覆的訊息 ID val replyToId = _replyingMessage.value?.id + val currentLoggedInUserId = tokenManager.currentUserId.first() ?: "" - val result = repository.sendMessage( + // 呼叫樂觀更新方法 + val result = repository.sendMessageOptimistically( roomId = roomId, + currentUserId = currentLoggedInUserId, content = content, replyToId = replyToId ) - result.onSuccess { newMessage -> - _sendMessageState.value = SendMessageState.Success(newMessage) - _replyingMessage.value = null - - // 😎【超讚的體驗優化】:發送成功後,直接把新訊息手動塞進現有的 UI 列表裡 - val currentState = _uiState.value - if (currentState is MessagesUiState.Success) { - // 把新訊息加到原本的列表最後面 - val updatedMessages = currentState.messages + newMessage - - // 更新 uiState,這樣 Compose 畫面不需要重新 load 網路就能立刻刷新! - _uiState.value = currentState.copy(messages = updatedMessages) - } + // 清空回覆狀態 + _replyingMessage.value = null + result.onSuccess { + // 🌟 發送成功!此時 Room 內對應的臨時訊息已經被改為 SUCCESS 了 + // Flow 會自動讓 UI 刷新,ViewModel 這裡「不需要」手動做 updatedMessages + newMessage + _sendMessageState.value = SendMessageState.Idle }.onFailure { error -> Log.e("ChatViewModel", "Send message failed", error) + // 🌟 發送失敗!Room 內的臨時訊息已被改為 FAILED,畫面上會顯示驚嘆號 _sendMessageState.value = SendMessageState.Error(error.message ?: "未知錯誤") } } } - - //upload file - fun uploadAttachment(roomId: String, fileUri: Uri) { - viewModelScope.launch { - // 1. 將發送狀態切換為 Loading,讓 UI 顯示進度條 - _sendMessageState.value = SendMessageState.Loading - - // 2. 呼叫 Repository 處理檔案並上傳 - val result = repository.uploadAttachment(roomId, fileUri) - - result.onSuccess { newMessage -> - // 3. 上傳成功,更新發送狀態 - _sendMessageState.value = SendMessageState.Success(newMessage) - - // 😎【體驗優化】:跟發送文字訊息一樣,直接把帶有圖片的新訊息塞進現有的 UI 列表裡 - val currentState = _uiState.value - if (currentState is MessagesUiState.Success) { - val updatedMessages = currentState.messages + newMessage - _uiState.value = currentState.copy(messages = updatedMessages) - } - - }.onFailure { error -> - Log.e("ChatViewModel", "Upload attachment failed", error) - // 4. 上傳失敗,通知 UI 顯示錯誤 - _sendMessageState.value = SendMessageState.Error(error.message ?: "上傳失敗,請稍後再試") - } - } - } - - private val _reactionUsersState = MutableStateFlow(ReactionUsersState.Idle) - val reactionUsersState: StateFlow = _reactionUsersState.asStateFlow() - - //get emoji reaction - fun loadMessageReactionUsers(roomId: String, messageId: String, emoji: String) { - viewModelScope.launch { - _reactionUsersState.value = ReactionUsersState.Loading - - val result = repository.getMessageReactionUsers( - roomId = roomId, - messageId = messageId, - emoji = emoji - ) - - result.onSuccess { response -> - _reactionUsersState.value = ReactionUsersState.Success(response.users) - }.onFailure { error -> - Log.e("ChatViewModel", "Get reaction users failed", error) - _reactionUsersState.value = ReactionUsersState.Error(error.message ?: "無法取得按讚名單") - } - } - } - //add emoji reaction - fun addMessageReaction(roomId: String, messageId: String, emoji: String) { - viewModelScope.launch { - val result = repository.addMessageReaction(roomId, messageId, emoji) - - result.onSuccess { - // 😎【超讚體驗優化】:點擊成功後,除了通知後端, - // 也可以選擇直接呼叫 loadMessages(roomId) 來重新拉取最新帶有按讚數量的訊息列表 - loadMessages(roomId) - }.onFailure { error -> - Log.e("ChatViewModel", "Add reaction failed", error) - // 這裡可以依需求決定要不要用 Toast 提示使用者點擊失敗 - } - } - } - //delete emoji reaction - fun removeMessageReaction(roomId: String, messageId: String, emoji: String) { - viewModelScope.launch { - val result = repository.deleteMessageReaction(roomId, messageId, emoji) - - result.onSuccess { - // 移除成功後,重新拉取最新訊息列表以刷新 UI - loadMessages(roomId) - }.onFailure { error -> - Log.e("ChatViewModel", "Remove reaction failed", error) - } - } - } - - // 儲存刪除狀態的變數 - private val _deleteMessageState = MutableStateFlow(DeleteMessageState.Idle) - val deleteMessageState: StateFlow = _deleteMessageState.asStateFlow() - - // 刪除訊息的 function + // ===================================================================== + // 💡 核心改造三:刪除訊息 + // ===================================================================== fun deleteMessage(roomId: String, messageId: String) { viewModelScope.launch { _deleteMessageState.value = DeleteMessageState.Loading @@ -286,25 +186,10 @@ class ChatViewModel @Inject constructor( result.onSuccess { _deleteMessageState.value = DeleteMessageState.Success - - // ✨【優化】:刪除成功後,自動關閉長按選單 clearActionMessage() - val currentState = _uiState.value - if (currentState is MessagesUiState.Success) { - val updatedMessages = currentState.messages.map { message -> - if (message.id == messageId) { - message.copy( - isDeleted = true, - content = "此訊息已被刪除" - ) - } else { - message - } - } - _uiState.value = currentState.copy(messages = updatedMessages) - } - + // 🌟 刪除成功!Repository 已經把本地 Room 的資料刪除 + // Flow 會自動發送全新的列表給 UI,這裡「完全不需要」再手動 map 改 content 了! }.onFailure { error -> Log.e("ChatViewModel", "Delete message failed", error) _deleteMessageState.value = DeleteMessageState.Error(error.message ?: "刪除訊息失敗") @@ -312,18 +197,55 @@ class ChatViewModel @Inject constructor( } } - // 重設刪除狀態的 function (供 UI 重置狀態使用) - fun resetDeleteMessageState() { - _deleteMessageState.value = DeleteMessageState.Idle + // --- 其餘 uploadAttachment、Emoji 功能保持原樣 --- + fun uploadAttachment(roomId: String, fileUri: Uri) { + 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) + }.onFailure { error -> + Log.e("ChatViewModel", "Upload attachment failed", error) + _sendMessageState.value = SendMessageState.Error(error.message ?: "上傳失敗") + } + } } - // 💡 請確保這段程式碼有確實待在 ChatViewModel 裡面 - fun resetReactionUsersState() { - _reactionUsersState.value = ReactionUsersState.Idle + private val _reactionUsersState = MutableStateFlow(ReactionUsersState.Idle) + val reactionUsersState: StateFlow = _reactionUsersState.asStateFlow() + + fun loadMessageReactionUsers(roomId: String, messageId: String, emoji: String) { + viewModelScope.launch { + _reactionUsersState.value = ReactionUsersState.Loading + repository.getMessageReactionUsers(roomId, messageId, emoji) + .onSuccess { response -> _reactionUsersState.value = ReactionUsersState.Success(response.users) } + .onFailure { error -> _reactionUsersState.value = ReactionUsersState.Error(error.message ?: "無法取得按讚名單") } + } } - //新增:重設發送狀態的 function - fun resetSendMessageState() { - _sendMessageState.value = SendMessageState.Idle + fun addMessageReaction(roomId: String, messageId: String, emoji: String) { + viewModelScope.launch { + repository.addMessageReaction(roomId, messageId, emoji) + .onSuccess { + // 🌟 這裡也不用 loadMessages(roomId) 了,直接背景 refresh 即可 + repository.refreshChatMessages(roomId) + } + } } + + fun removeMessageReaction(roomId: String, messageId: String, emoji: String) { + viewModelScope.launch { + repository.deleteMessageReaction(roomId, messageId, emoji) + .onSuccess { + repository.refreshChatMessages(roomId) + } + } + } + + fun resetDeleteMessageState() { _deleteMessageState.value = DeleteMessageState.Idle } + fun resetReactionUsersState() { _reactionUsersState.value = ReactionUsersState.Idle } + fun resetSendMessageState() { _sendMessageState.value = SendMessageState.Idle } } \ No newline at end of file