add delta sync for dm-v1
This commit is contained in:
parent
61926d1f61
commit
6466a36547
15 changed files with 400 additions and 74 deletions
10
.idea/planningMode.xml
Normal file
10
.idea/planningMode.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PlanningModeManager">
|
||||
<option name="approvalStates">
|
||||
<map>
|
||||
<entry key="53d32dd4-9fa5-47db-9a92-2613918d056e" value="false" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -30,6 +30,10 @@ data class RefreshResponse(
|
|||
|
||||
interface TecnologiaApi {
|
||||
|
||||
//sync dm data
|
||||
@GET("api/sync")
|
||||
suspend fun sync(@Query("since") since: Long): Response<SyncResponse>
|
||||
|
||||
@GET("api/health")
|
||||
suspend fun checkHealth(): Response<Unit>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +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.example.login_v3.data.local.entities.MessageStatus
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
|
|
@ -162,7 +162,52 @@ data class MessageReactionUsersResponse(
|
|||
|
||||
|
||||
|
||||
//voice call
|
||||
// --- Delta-Sync 相關 ---
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SyncResponse(
|
||||
@Json(name = "blocks") val blocks: List<SyncBlock>,
|
||||
@Json(name = "friends") val friends: List<SyncFriend>,
|
||||
@Json(name = "full_snapshot") val fullSnapshot: Boolean,
|
||||
@Json(name = "next_since") val nextSince: Long,
|
||||
@Json(name = "rooms") val rooms: List<SyncRoom>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SyncBlock(
|
||||
@Json(name = "avatar_url") val avatarUrl: String?,
|
||||
@Json(name = "blocked_at") val blockedAt: String,
|
||||
@Json(name = "display_name") val displayName: String,
|
||||
@Json(name = "removed") val removed: Boolean = false,
|
||||
@Json(name = "user_id") val userId: String,
|
||||
@Json(name = "username") val username: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SyncFriend(
|
||||
@Json(name = "avatar_url") val avatarUrl: String?,
|
||||
@Json(name = "display_name") val displayName: String,
|
||||
@Json(name = "presence_status") val presenceStatus: String,
|
||||
@Json(name = "removed") val removed: Boolean = false,
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "user_id") val userId: String,
|
||||
@Json(name = "username") val username: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SyncRoom(
|
||||
@Json(name = "is_hidden") val isHidden: Boolean,
|
||||
@Json(name = "is_muted") val isMuted: Boolean,
|
||||
@Json(name = "is_pinned") val isPinned: Boolean,
|
||||
@Json(name = "last_message") val lastMessage: LastMessage?,
|
||||
@Json(name = "mention_count") val mentionCount: Int,
|
||||
@Json(name = "nickname") val nickname: String?,
|
||||
@Json(name = "partner") val partner: Partner?,
|
||||
@Json(name = "removed") val removed: Boolean = false,
|
||||
@Json(name = "room_id") val roomId: String,
|
||||
@Json(name = "unread_count") val unreadCount: Int
|
||||
)
|
||||
|
||||
// --- Voice Call 相關 ---
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CallLogInfo(
|
||||
@Json(name = "type") val type: String, // 例如: call_missed, call_rejected
|
||||
|
|
|
|||
|
|
@ -3,11 +3,29 @@ package com.example.login_v3.data.di
|
|||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
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.data.local.dao.BlockedUserDao
|
||||
import com.example.login_v3.data.local.dao.FriendDao
|
||||
import com.example.login_v3.data.local.dao.MessageDao
|
||||
import com.example.login_v3.data.local.dao.RoomLocalDao
|
||||
import com.example.login_v3.data.local.entities.BlockedUserLocalEntity
|
||||
import com.example.login_v3.data.local.entities.FriendLocalEntity
|
||||
import com.example.login_v3.data.local.entities.MessageEntity
|
||||
import com.example.login_v3.data.local.entities.RoomLocalEntity
|
||||
|
||||
@Database(entities = [MessageEntity::class], version = 1, exportSchema = false)
|
||||
@Database(
|
||||
entities = [
|
||||
MessageEntity::class,
|
||||
FriendLocalEntity::class,
|
||||
RoomLocalEntity::class,
|
||||
BlockedUserLocalEntity::class
|
||||
],
|
||||
version = 2,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(RoomConverters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun messageDao(): MessageDao
|
||||
abstract fun friendDao(): FriendDao
|
||||
abstract fun roomLocalDao(): RoomLocalDao
|
||||
abstract fun blockedUserDao(): BlockedUserDao
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package com.example.login_v3.data.di
|
|||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.example.login_v3.home.Message.ViewModel.Detail.MessageDao
|
||||
import com.example.login_v3.data.local.dao.MessageDao
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -24,7 +24,7 @@ object DatabaseModule {
|
|||
AppDatabase::class.java,
|
||||
"tecnologia_chat_db" // 資料庫檔案名稱
|
||||
)
|
||||
// .fallbackToDestructiveMigration() // 💡 提示:未來如果修改 Entity 欄位導致崩潰,可以先取消這行註解來清除舊資料庫
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,29 +4,62 @@ import androidx.room.TypeConverter
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory // 👈 確保有 import 這個
|
||||
import com.example.login_v3.data.api.api_class.LastMessage
|
||||
import com.example.login_v3.data.api.api_class.Partner
|
||||
import com.example.login_v3.data.api.api_class.Reaction
|
||||
|
||||
class RoomConverters {
|
||||
// 💡 核心修正:加入 KotlinJsonAdapterFactory 賦予 Moshi 反射辨識 Kotlin Data Class 的能力
|
||||
private val moshi = Moshi.Builder()
|
||||
.addLast(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
|
||||
private val reactionListType = Types.newParameterizedType(List::class.java, Reaction::class.java)
|
||||
private val adapter = moshi.adapter<List<Reaction>>(reactionListType)
|
||||
private val reactionAdapter = moshi.adapter<List<Reaction>>(reactionListType)
|
||||
private val partnerAdapter = moshi.adapter(Partner::class.java)
|
||||
private val lastMessageAdapter = moshi.adapter(LastMessage::class.java)
|
||||
|
||||
@TypeConverter
|
||||
fun fromReactionList(reactions: List<Reaction>?): String? {
|
||||
return adapter.toJson(reactions ?: emptyList())
|
||||
return reactionAdapter.toJson(reactions ?: emptyList())
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toReactionList(json: String?): List<Reaction>? {
|
||||
if (json.isNullOrBlank()) return emptyList()
|
||||
return try {
|
||||
adapter.fromJson(json)
|
||||
reactionAdapter.fromJson(json)
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromPartner(partner: Partner?): String? {
|
||||
return partnerAdapter.toJson(partner)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toPartner(json: String?): Partner? {
|
||||
if (json.isNullOrBlank()) return null
|
||||
return try {
|
||||
partnerAdapter.fromJson(json)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromLastMessage(lastMessage: LastMessage?): String? {
|
||||
return lastMessageAdapter.toJson(lastMessage)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toLastMessage(json: String?): LastMessage? {
|
||||
if (json.isNullOrBlank()) return null
|
||||
return try {
|
||||
lastMessageAdapter.fromJson(json)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,26 @@ class UserPreferencesRepository(private val dataStore: DataStore<Preferences>) {
|
|||
val THEME_MODE = stringPreferencesKey("theme_mode")
|
||||
// 新增:儲存自訂壁紙檔案路徑的 Key
|
||||
val CUSTOM_WALLPAPER_PATH = stringPreferencesKey("custom_wallpaper_path")
|
||||
val SYNC_SINCE = androidx.datastore.preferences.core.longPreferencesKey("sync_since")
|
||||
}
|
||||
|
||||
// 讀取同步時間
|
||||
val syncSinceFlow: Flow<Long> = dataStore.data
|
||||
.catch { exception ->
|
||||
if (exception is IOException) {
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
.map { preferences ->
|
||||
preferences[PreferencesKeys.SYNC_SINCE] ?: 0L
|
||||
}
|
||||
|
||||
suspend fun saveSyncSince(since: Long) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.SYNC_SINCE] = since
|
||||
}
|
||||
}
|
||||
|
||||
// 讀取設定:直接在宣告時賦值,簡潔又安全
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.example.login_v3.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import com.example.login_v3.data.local.entities.MessageEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface MessageDao {
|
||||
@Query("SELECT * FROM messages WHERE chatRoomId = :roomId ORDER BY createdAt DESC")
|
||||
fun getMessagesFlow(roomId: String): Flow<List<MessageEntity>>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE id = :messageId LIMIT 1")
|
||||
suspend fun getMessageById(messageId: String): MessageEntity?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertOrUpdate(message: MessageEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertOrUpdateList(messages: List<MessageEntity>)
|
||||
|
||||
@Query("DELETE FROM messages WHERE id = :id")
|
||||
suspend fun deleteById(id: String)
|
||||
|
||||
@Transaction
|
||||
suspend fun replaceTempMessageWithSuccess(tempId: String, successEntity: MessageEntity) {
|
||||
deleteById(tempId)
|
||||
insertOrUpdate(successEntity)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.example.login_v3.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.example.login_v3.data.local.entities.BlockedUserLocalEntity
|
||||
import com.example.login_v3.data.local.entities.FriendLocalEntity
|
||||
import com.example.login_v3.data.local.entities.RoomLocalEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface FriendDao {
|
||||
@Query("SELECT * FROM friends")
|
||||
fun getAllFriendsFlow(): Flow<List<FriendLocalEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertOrUpdate(friends: List<FriendLocalEntity>)
|
||||
|
||||
@Query("DELETE FROM friends WHERE userId = :userId")
|
||||
suspend fun deleteById(userId: String)
|
||||
|
||||
@Query("DELETE FROM friends")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface RoomLocalDao {
|
||||
@Query("SELECT * FROM rooms ORDER BY isPinned DESC")
|
||||
fun getAllRoomsFlow(): Flow<List<RoomLocalEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertOrUpdate(rooms: List<RoomLocalEntity>)
|
||||
|
||||
@Query("DELETE FROM rooms WHERE roomId = :roomId")
|
||||
suspend fun deleteById(roomId: String)
|
||||
|
||||
@Query("DELETE FROM rooms")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface BlockedUserDao {
|
||||
@Query("SELECT * FROM blocks")
|
||||
fun getAllBlocksFlow(): Flow<List<BlockedUserLocalEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertOrUpdate(blocks: List<BlockedUserLocalEntity>)
|
||||
|
||||
@Query("DELETE FROM blocks WHERE userId = :userId")
|
||||
suspend fun deleteById(userId: String)
|
||||
|
||||
@Query("DELETE FROM blocks")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.example.login_v3.data.local.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.example.login_v3.data.api.api_class.Reaction
|
||||
|
||||
enum class MessageStatus { SENDING, SUCCESS, FAILED }
|
||||
|
||||
@Entity(tableName = "messages")
|
||||
data class MessageEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val chatRoomId: String,
|
||||
val senderId: String,
|
||||
val content: String,
|
||||
val type: String,
|
||||
val createdAt: String,
|
||||
val isEdited: Boolean,
|
||||
val isDeleted: Boolean,
|
||||
val replyToId: String?,
|
||||
val status: MessageStatus,
|
||||
val reactions: List<Reaction>?
|
||||
)
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.example.login_v3.data.local.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.example.login_v3.data.api.api_class.LastMessage
|
||||
import com.example.login_v3.data.api.api_class.Partner
|
||||
|
||||
@Entity(tableName = "friends")
|
||||
data class FriendLocalEntity(
|
||||
@PrimaryKey val userId: String,
|
||||
val username: String,
|
||||
val displayName: String,
|
||||
val avatarUrl: String?,
|
||||
val status: String,
|
||||
val presenceStatus: String
|
||||
)
|
||||
|
||||
@Entity(tableName = "rooms")
|
||||
data class RoomLocalEntity(
|
||||
@PrimaryKey val roomId: String,
|
||||
val nickname: String?,
|
||||
val isHidden: Boolean,
|
||||
val isMuted: Boolean,
|
||||
val isPinned: Boolean,
|
||||
val unreadCount: Int,
|
||||
val mentionCount: Int,
|
||||
val partner: Partner?,
|
||||
val lastMessage: LastMessage?
|
||||
)
|
||||
|
||||
@Entity(tableName = "blocks")
|
||||
data class BlockedUserLocalEntity(
|
||||
@PrimaryKey val userId: String,
|
||||
val username: String,
|
||||
val displayName: String,
|
||||
val avatarUrl: String?,
|
||||
val blockedAt: String
|
||||
)
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.example.login_v3.data.repository.basic
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import com.example.login_v3.data.api.TecnologiaApi
|
||||
import com.example.login_v3.data.di.AppDatabase
|
||||
import com.example.login_v3.data.local.UserPreferences.UserPreferencesRepository
|
||||
import com.example.login_v3.data.local.entities.BlockedUserLocalEntity
|
||||
import com.example.login_v3.data.local.entities.FriendLocalEntity
|
||||
import com.example.login_v3.data.local.entities.RoomLocalEntity
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class SyncRepository @Inject constructor(
|
||||
private val api: TecnologiaApi,
|
||||
private val db: AppDatabase,
|
||||
private val userPrefs: UserPreferencesRepository
|
||||
) {
|
||||
private val friendDao = db.friendDao()
|
||||
private val roomDao = db.roomLocalDao()
|
||||
private val blockDao = db.blockedUserDao()
|
||||
|
||||
suspend fun performSync(): Result<Unit> {
|
||||
return try {
|
||||
val since = userPrefs.syncSinceFlow.first()
|
||||
val response = api.sync(since)
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body() ?: return Result.failure(Exception("Sync body is null"))
|
||||
|
||||
db.withTransaction {
|
||||
if (body.fullSnapshot) {
|
||||
friendDao.deleteAll()
|
||||
roomDao.deleteAll()
|
||||
blockDao.deleteAll()
|
||||
}
|
||||
|
||||
// Process Blocks
|
||||
body.blocks.forEach { block ->
|
||||
if (block.removed) {
|
||||
blockDao.deleteById(block.userId)
|
||||
} else {
|
||||
blockDao.insertOrUpdate(listOf(
|
||||
BlockedUserLocalEntity(
|
||||
userId = block.userId,
|
||||
username = block.username,
|
||||
displayName = block.displayName,
|
||||
avatarUrl = block.avatarUrl,
|
||||
blockedAt = block.blockedAt
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Process Friends
|
||||
body.friends.forEach { friend ->
|
||||
if (friend.removed) {
|
||||
friendDao.deleteById(friend.userId)
|
||||
} else {
|
||||
friendDao.insertOrUpdate(listOf(
|
||||
FriendLocalEntity(
|
||||
userId = friend.userId,
|
||||
username = friend.username,
|
||||
displayName = friend.displayName,
|
||||
avatarUrl = friend.avatarUrl,
|
||||
status = friend.status,
|
||||
presenceStatus = friend.presenceStatus
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Process Rooms
|
||||
body.rooms.forEach { room ->
|
||||
if (room.removed) {
|
||||
roomDao.deleteById(room.roomId)
|
||||
} else {
|
||||
roomDao.insertOrUpdate(listOf(
|
||||
RoomLocalEntity(
|
||||
roomId = room.roomId,
|
||||
nickname = room.nickname,
|
||||
isHidden = room.isHidden,
|
||||
isMuted = room.isMuted,
|
||||
isPinned = room.isPinned,
|
||||
unreadCount = room.unreadCount,
|
||||
mentionCount = room.mentionCount,
|
||||
partner = room.partner,
|
||||
lastMessage = room.lastMessage
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
userPrefs.saveSyncSince(body.nextSince)
|
||||
}
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.failure(Exception("Sync API failed: ${response.code()}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,9 +17,9 @@ import com.example.login_v3.data.api.api_class.SendMessageRequest
|
|||
import com.example.login_v3.data.api.api_class.WebSocketEventResponse
|
||||
import com.example.login_v3.data.di.ChatWebSocketManager
|
||||
import com.example.login_v3.data.repository.basic.TokenManager
|
||||
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 com.example.login_v3.data.local.dao.MessageDao
|
||||
import com.example.login_v3.data.local.entities.MessageEntity
|
||||
import com.example.login_v3.data.local.entities.MessageStatus
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ import com.example.login_v3.home.Message.UI.Detail.Message_Component.ReactionRow
|
|||
import com.example.login_v3.home.Message.UI.Detail.Message_Component.UserAvatar
|
||||
import com.example.login_v3.home.Message.ViewModel.Detail.ChatViewModel
|
||||
import com.example.login_v3.home.Message.ViewModel.Detail.DeleteMessageState
|
||||
import com.example.login_v3.home.Message.ViewModel.Detail.MessageStatus
|
||||
import com.example.login_v3.data.local.entities.MessageStatus
|
||||
import com.example.login_v3.home.Message.ViewModel.Detail.MessagesUiState
|
||||
import com.example.login_v3.home.Message.ViewModel.Detail.SendMessageState
|
||||
import com.example.login_v3.navigation.BottomBarViewModel
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
package com.example.login_v3.home.Message.ViewModel.Detail
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import com.example.login_v3.data.api.api_class.Reaction
|
||||
|
||||
enum class MessageStatus { SENDING, SUCCESS, FAILED }
|
||||
|
||||
@Entity(tableName = "messages")
|
||||
data class MessageEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val chatRoomId: String, // 👈 對齊你的 @Json(name = "chat_room_id")
|
||||
val senderId: String,
|
||||
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)
|
||||
val reactions: List<Reaction>?
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface MessageDao {
|
||||
// 1. 根據 roomId 監聽整個聊天室的訊息 Flow
|
||||
@Query("SELECT * FROM messages WHERE chatRoomId = :roomId ORDER BY createdAt DESC")
|
||||
fun getMessagesFlow(roomId: String): Flow<List<MessageEntity>>
|
||||
|
||||
// 2. ✨【新增】根據訊息 id 查詢單筆資料(樂觀更新備份用)
|
||||
@Query("SELECT * FROM messages WHERE id = :messageId LIMIT 1")
|
||||
suspend fun getMessageById(messageId: String): MessageEntity?
|
||||
|
||||
// 3. 插入或更新單筆訊息ssss
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertOrUpdate(message: MessageEntity)
|
||||
|
||||
// 4. ✨【優化】支援批次插入或更新(可以順便把 refreshChatMessages 裡面的 forEach 換掉,效能更好)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertOrUpdateList(messages: List<MessageEntity>)
|
||||
|
||||
// 5. 根據 id 刪除特定訊息
|
||||
@Query("DELETE FROM messages WHERE id = :id")
|
||||
suspend fun deleteById(id: String)
|
||||
|
||||
@Transaction
|
||||
suspend fun replaceTempMessageWithSuccess(tempId: String, successEntity: MessageEntity) {
|
||||
deleteById(tempId) // 1. 先刪除本地暫存 ID
|
||||
insertOrUpdate(successEntity) // 2. 立刻寫入後端正式 ID
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue