This commit is contained in:
Eric85710 2026-06-07 05:30:24 +08:00
parent e49d834fc7
commit ab10357042
4 changed files with 242 additions and 220 deletions

View file

@ -1,6 +1,7 @@
package com.example.login_v3.data.api.api_class package com.example.login_v3.data.api.api_class
import com.example.login_v3.R 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.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@ -103,11 +104,14 @@ data class Message(
@Json(name = "reply_to_id") val replyToId: String? = null, @Json(name = "reply_to_id") val replyToId: String? = null,
@kotlin.jvm.Transient @kotlin.jvm.Transient
var repliedMessage: Message? = null, var repliedMessage: Message? = null,
@Json(name = "forwarded_from_id") val forwardedFromId: String? = null, @Json(name = "forwarded_from_id") val forwardedFromId: String? = null,
@Json(name = "forwarded_from_room_id") val forwardedFromRoomId: String? = null, @Json(name = "forwarded_from_room_id") val forwardedFromRoomId: String? = null,
@Json(name = "attachment") val attachment: Attachment? = null, // 直接使用外層的 Attachment @Json(name = "attachment") val attachment: Attachment? = null,
@Json(name = "reactions") val reactions: List<Reaction>? = emptyList() // 直接使用外層的 Reaction @Json(name = "reactions") val reactions: List<Reaction>? = emptyList(),
// 🌟 核心新增:讓 UI 可以辨識訊息發送狀態!
@kotlin.jvm.Transient // 👈 告訴 Moshi 這個欄位不需要解析 JSON,這是我們本地維護的
val status: MessageStatus = MessageStatus.SUCCESS
) )
//send message //send message

View file

@ -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.RoomListResponse
import com.example.login_v3.data.api.api_class.SendMessageRequest 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.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 dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -43,33 +48,122 @@ class ChatRoomsRepository @Inject constructor(
} }
} }
suspend fun getChatMessages(roomId: String): Result<MessageResponse> { fun getChatMessagesFlow(roomId: String): Flow<List<Message>> {
return try { return messageDao.getMessagesFlow(roomId).map { entities ->
val response = api.getChatMessages(roomId) 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
)
}
if (response.isSuccessful) { // 你原本的處理回覆邏輯
val body = response.body() val messageMap = domainMessages.associateBy { it.id }
if (body != null) { domainMessages.forEach { message ->
// 💡 核心邏輯:將訊息列表轉成 Map,方便用 ID 快速查找
val messageMap = body.messages.associateBy { it.id }
// 走訪每一條訊息,如果它有 replyToId,就去 Map 裡面找出那一條訊息塞給它
body.messages.forEach { message ->
if (!message.replyToId.isNullOrBlank()) { if (!message.replyToId.isNullOrBlank()) {
message.repliedMessage = messageMap[message.replyToId] message.repliedMessage = messageMap[message.replyToId]
} }
} }
domainMessages
}
}
Result.success(body) // api get data and put to local
suspend fun refreshChatMessages(roomId: String): Result<Unit> {
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<Unit> {
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)
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 { } else {
Result.failure(Exception("回應身體為空 (Empty response body)")) messageDao.insertOrUpdate(tempMessageEntity.copy(status = MessageStatus.FAILED))
Result.failure(Exception("Body null"))
} }
} else { } else {
val errorMsg = response.errorBody()?.string() ?: "未知錯誤" messageDao.insertOrUpdate(tempMessageEntity.copy(status = MessageStatus.FAILED))
Result.failure(Exception("網路請求失敗,錯誤碼: ${response.code()}, 訊息: $errorMsg")) Result.failure(Exception("API Error"))
} }
} catch (e: Exception) { } catch (e: Exception) {
messageDao.insertOrUpdate(tempMessageEntity.copy(status = MessageStatus.FAILED))
Result.failure(e) Result.failure(e)
} }
} }
@ -296,16 +390,14 @@ class ChatRoomsRepository @Inject constructor(
suspend fun deleteMessage(roomId: String, messageId: String): Result<Unit> { suspend fun deleteMessage(roomId: String, messageId: String): Result<Unit> {
return try { return try {
val response = api.deleteMessage(roomId, messageId) val response = api.deleteMessage(roomId, messageId)
if (response.isSuccessful) { if (response.isSuccessful) {
// 後端回傳 204 No Content 成功時沒有 body,直接回傳 Unit 即可 // 💡 既然刪除了,本地也一併砍掉,UI 就會立刻同步消失
messageDao.deleteById(messageId)
Result.success(Unit) Result.success(Unit)
} else { } else {
val errorMsg = response.errorBody()?.string() ?: "未知錯誤" Result.failure(Exception("Delete failed"))
Result.failure(Exception("刪除訊息失敗,錯誤碼: ${response.code()}, 訊息: $errorMsg"))
} }
} catch (e: Exception) { } catch (e: Exception) {
// 捕捉網路斷線等異常狀況
Result.failure(e) Result.failure(e)
} }
} }

View file

@ -12,18 +12,22 @@ enum class MessageStatus { SENDING, SUCCESS, FAILED }
@Entity(tableName = "messages") @Entity(tableName = "messages")
data class MessageEntity( data class MessageEntity(
@PrimaryKey val id: String, // 臨時訊息可以用 UUID.randomUUID().toString() @PrimaryKey val id: String,
val roomId: String, val chatRoomId: String, // 👈 對齊你的 @Json(name = "chat_room_id")
val content: String,
val senderId: String, val senderId: String,
val timestamp: Long, val content: String,
val status: MessageStatus // 👈 核心:用來控制 UI 顯示樣式 val type: String, // 👈 新增:文字、圖片等類型
val createdAt: String, // 👈 核心修正:改成與 API 一致的 String 格式
val isEdited: Boolean, // 👈 新增
val isDeleted: Boolean, // 👈 新增
val replyToId: String?, // 👈 新增
val status: MessageStatus // 👈 樂觀更新狀態控制 (SENDING, SUCCESS, FAILED)
) )
@Dao @Dao
interface MessageDao { interface MessageDao {
// 💡 監聽特定聊天室的訊息流 // 💡 修正這裡:WHERE 條件欄位改為 chatRoomId
@Query("SELECT * FROM messages WHERE roomId = :roomId ORDER BY timestamp ASC") @Query("SELECT * FROM messages WHERE chatRoomId = :roomId ORDER BY createdAt ASC")
fun getMessagesFlow(roomId: String): Flow<List<MessageEntity>> fun getMessagesFlow(roomId: String): Flow<List<MessageEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)

View file

@ -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.api.api_class.fullContactAvatarUrl
import com.example.login_v3.data.repository.basic.TokenManager import com.example.login_v3.data.repository.basic.TokenManager
import com.example.login_v3.home.Message.ViewModel.UserStatus import com.example.login_v3.home.Message.ViewModel.UserStatus
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -64,220 +65,119 @@ class ChatViewModel @Inject constructor(
private val tokenManager: TokenManager private val tokenManager: TokenManager
) : ViewModel() { ) : ViewModel() {
// --- 訊息長按動作選單狀態 (Action Menu State) --- // --- 訊息長按動作選單與回覆狀態 (保持原樣) ---
// 💡 補上這兩行宣告
// 1. 私有的 MutableStateFlow,用來控制長按選中的訊息 private val _deleteMessageState = MutableStateFlow<DeleteMessageState>(DeleteMessageState.Idle)
val deleteMessageState: StateFlow<DeleteMessageState> = _deleteMessageState.asStateFlow()
private val _actionMessage = MutableStateFlow<Message?>(null) private val _actionMessage = MutableStateFlow<Message?>(null)
// 2. 公開給 Compose UI 觀察的唯讀 StateFlow
val actionMessage: StateFlow<Message?> = _actionMessage.asStateFlow() val actionMessage: StateFlow<Message?> = _actionMessage.asStateFlow()
/** fun setActionMessage(message: Message?) { _actionMessage.value = message }
* 設定目前哪一則訊息被長按選取(開啟動作選單) fun clearActionMessage() { _actionMessage.value = null }
*/
fun setActionMessage(message: Message?) {
_actionMessage.value = message
}
/**
* 清除目前選取的訊息狀態(關閉動作選單)
*/
fun clearActionMessage() {
_actionMessage.value = null
}
//reply function
private val _replyingMessage = MutableStateFlow<Message?>(null) private val _replyingMessage = MutableStateFlow<Message?>(null)
val replyingMessage: StateFlow<Message?> = _replyingMessage.asStateFlow() val replyingMessage: StateFlow<Message?> = _replyingMessage.asStateFlow()
// 💡 新增:當使用者點選某條訊息的「回覆」按鈕時呼叫 fun setReplyingMessage(message: Message?) { _replyingMessage.value = message }
fun setReplyingMessage(message: Message?) {
_replyingMessage.value = message
}
//UI state var // --- 核心狀態管理 ---
private val _uiState = MutableStateFlow<MessagesUiState>(MessagesUiState.Loading) private val _uiState = MutableStateFlow<MessagesUiState>(MessagesUiState.Loading)
val uiState: StateFlow<MessagesUiState> = _uiState.asStateFlow() val uiState: StateFlow<MessagesUiState> = _uiState.asStateFlow()
//message var // 💡 提示:在做法 A 下,_sendMessageState 通常只用來控制輸入框的「Loading」轉圈圈或置灰,真正的訊息成功/失敗已經內嵌在 Message 本身的 status 裡了。
private val _sendMessageState = MutableStateFlow<SendMessageState>(SendMessageState.Idle) private val _sendMessageState = MutableStateFlow<SendMessageState>(SendMessageState.Idle)
val sendMessageState: StateFlow<SendMessageState> = _sendMessageState val sendMessageState: StateFlow<SendMessageState> = _sendMessageState
// 用來管理監聽 Room Flow 的 Job,避免重複綁定
private var messageListenerJob: Job? = null
// =====================================================================
// 💡 核心改造一:持續監聽本地資料庫的 Flow
// =====================================================================
fun loadMessages(roomId: String) { fun loadMessages(roomId: String) {
viewModelScope.launch { // 先取消上一次的監聽(如果有切換房間的話)
messageListenerJob?.cancel()
messageListenerJob = viewModelScope.launch {
_uiState.value = MessagesUiState.Loading _uiState.value = MessagesUiState.Loading
//isMe
val currentLoggedInUserId = tokenManager.currentUserId.first() ?: "" val currentLoggedInUserId = tokenManager.currentUserId.first() ?: ""
// 1. 同時獲取房間資訊與訊息列表 val roomResult = repository.getChatRoom(roomId)
val roomResult = repository.getChatRoom(roomId) // ⚠️ 需確保 Repository 有提供此方法
val messagesResult = repository.getChatMessages(roomId)
if (messagesResult.isSuccess) {
val messagesResponse = messagesResult.getOrNull()!!
// 2. 根據房間資訊決定標題邏輯
val room = roomResult.getOrNull() val room = roomResult.getOrNull()
// ✨ 這裡改用 Any? 來接收擴充屬性的結果
val (title, status, avatarUrl: Any?) = if (room != null) { val (title, status, avatarUrl: Any?) = if (room != null) {
val name = room.partner?.displayName ?: room.partner?.username ?: "未知用戶" val name = room.partner?.displayName ?: room.partner?.username ?: "未知用戶"
val userStatus = UserStatus.fromString(room.partner?.status) val userStatus = UserStatus.fromString(room.partner?.status)
// 👇 直接呼叫你寫好的擴充屬性
val avatar = room.partner?.fullContactAvatarUrl val avatar = room.partner?.fullContactAvatarUrl
Triple(name, userStatus, avatar) Triple(name, userStatus, avatar)
} else { } else {
// 如果沒有房間資訊,就給預設的頭像資源
Triple("聊天室", UserStatus.UNKNOWN, R.drawable.avatar_v1) Triple("聊天室", UserStatus.UNKNOWN, R.drawable.avatar_v1)
} }
// 🌟 關鍵點:開始收集來自 Room Database 的冷流(Flow)
repository.getChatMessagesFlow(roomId).collect { localMessages ->
// 只要本地資料庫有任何風吹草動(多一條假訊息、狀態改變、被刪除),這裡都會立刻觸發
_uiState.value = MessagesUiState.Success( _uiState.value = MessagesUiState.Success(
roomTitle = title, roomTitle = title,
partnerStatus = status, partnerStatus = status,
partnerAvatarUrl = avatarUrl, //這裡就會是完整的網址或 R.drawable.avatar_v1 partnerAvatarUrl = avatarUrl,
messages = messagesResponse.messages, messages = localMessages, // 👈 直接使用來自 Room 的最新訊息列表
currentUserId = currentLoggedInUserId currentUserId = currentLoggedInUserId
) )
}
}
//已讀功能 // 🌟 背景默默同步:去後端拉取最新訊息寫入 Room
launch { viewModelScope.launch {
repository.markAsRead(roomId) repository.refreshChatMessages(roomId).onSuccess {
.onFailure { error -> // 已讀功能保持
// 這裡通常不需要回報給 UI(不用跳 Error 畫面) repository.markAsRead(roomId).onFailure { error ->
// 只需要印個 Log 知道出事了就好
Log.e("ChatViewModel", "標記已讀失敗: ${error.message}") Log.e("ChatViewModel", "標記已讀失敗: ${error.message}")
} }
} }.onFailure { error ->
} else { Log.e("ChatViewModel", "背景同步失敗,改用純離線快取", error)
val exception = messagesResult.exceptionOrNull() // 這裡不用特地切換到 Error 狀態,因為 getChatMessagesFlow 可能已經把離線快取顯示在畫面上了
_uiState.value = MessagesUiState.Error(message = exception?.message ?: "未知錯誤")
} }
} }
} }
// =====================================================================
//upload image // 💡 核心改造二:發送文字訊息(樂觀更新)
fun sendMessage(roomId: String, content: String, replyToId: String? = null) { // =====================================================================
fun sendMessage(roomId: String, content: String) {
if (content.isBlank()) return if (content.isBlank()) return
viewModelScope.launch { viewModelScope.launch {
_sendMessageState.value = SendMessageState.Loading _sendMessageState.value = SendMessageState.Loading
// 💡 取得當前是否有正在回覆的訊息 ID
val replyToId = _replyingMessage.value?.id val replyToId = _replyingMessage.value?.id
val currentLoggedInUserId = tokenManager.currentUserId.first() ?: ""
val result = repository.sendMessage( // 呼叫樂觀更新方法
val result = repository.sendMessageOptimistically(
roomId = roomId, roomId = roomId,
currentUserId = currentLoggedInUserId,
content = content, content = content,
replyToId = replyToId replyToId = replyToId
) )
result.onSuccess { newMessage -> // 清空回覆狀態
_sendMessageState.value = SendMessageState.Success(newMessage)
_replyingMessage.value = null _replyingMessage.value = null
// 😎【超讚的體驗優化】:發送成功後,直接把新訊息手動塞進現有的 UI 列表裡 result.onSuccess {
val currentState = _uiState.value // 🌟 發送成功!此時 Room 內對應的臨時訊息已經被改為 SUCCESS 了
if (currentState is MessagesUiState.Success) { // Flow 會自動讓 UI 刷新,ViewModel 這裡「不需要」手動做 updatedMessages + newMessage
// 把新訊息加到原本的列表最後面 _sendMessageState.value = SendMessageState.Idle
val updatedMessages = currentState.messages + newMessage
// 更新 uiState,這樣 Compose 畫面不需要重新 load 網路就能立刻刷新!
_uiState.value = currentState.copy(messages = updatedMessages)
}
}.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 ?: "未知錯誤")
} }
} }
} }
// =====================================================================
//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>(ReactionUsersState.Idle)
val reactionUsersState: StateFlow<ReactionUsersState> = _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>(DeleteMessageState.Idle)
val deleteMessageState: StateFlow<DeleteMessageState> = _deleteMessageState.asStateFlow()
// 刪除訊息的 function
fun deleteMessage(roomId: String, messageId: String) { fun deleteMessage(roomId: String, messageId: String) {
viewModelScope.launch { viewModelScope.launch {
_deleteMessageState.value = DeleteMessageState.Loading _deleteMessageState.value = DeleteMessageState.Loading
@ -286,25 +186,10 @@ class ChatViewModel @Inject constructor(
result.onSuccess { result.onSuccess {
_deleteMessageState.value = DeleteMessageState.Success _deleteMessageState.value = DeleteMessageState.Success
// ✨【優化】:刪除成功後,自動關閉長按選單
clearActionMessage() clearActionMessage()
val currentState = _uiState.value // 🌟 刪除成功!Repository 已經把本地 Room 的資料刪除
if (currentState is MessagesUiState.Success) { // Flow 會自動發送全新的列表給 UI,這裡「完全不需要」再手動 map 改 content 了!
val updatedMessages = currentState.messages.map { message ->
if (message.id == messageId) {
message.copy(
isDeleted = true,
content = "此訊息已被刪除"
)
} else {
message
}
}
_uiState.value = currentState.copy(messages = updatedMessages)
}
}.onFailure { error -> }.onFailure { error ->
Log.e("ChatViewModel", "Delete message failed", error) Log.e("ChatViewModel", "Delete message failed", error)
_deleteMessageState.value = DeleteMessageState.Error(error.message ?: "刪除訊息失敗") _deleteMessageState.value = DeleteMessageState.Error(error.message ?: "刪除訊息失敗")
@ -312,18 +197,55 @@ class ChatViewModel @Inject constructor(
} }
} }
// 重設刪除狀態的 function (供 UI 重置狀態使用) // --- 其餘 uploadAttachment、Emoji 功能保持原樣 ---
fun resetDeleteMessageState() { fun uploadAttachment(roomId: String, fileUri: Uri) {
_deleteMessageState.value = DeleteMessageState.Idle 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 裡面 private val _reactionUsersState = MutableStateFlow<ReactionUsersState>(ReactionUsersState.Idle)
fun resetReactionUsersState() { val reactionUsersState: StateFlow<ReactionUsersState> = _reactionUsersState.asStateFlow()
_reactionUsersState.value = ReactionUsersState.Idle
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 addMessageReaction(roomId: String, messageId: String, emoji: String) {
fun resetSendMessageState() { viewModelScope.launch {
_sendMessageState.value = SendMessageState.Idle 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 }
} }