dm-93
This commit is contained in:
parent
e49d834fc7
commit
ab10357042
4 changed files with 242 additions and 220 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue