diff --git a/.idea/planningMode.xml b/.idea/planningMode.xml
new file mode 100644
index 0000000..3298ef1
--- /dev/null
+++ b/.idea/planningMode.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt b/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt
index ad5cb82..c20c4a6 100644
--- a/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt
+++ b/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt
@@ -30,6 +30,10 @@ data class RefreshResponse(
interface TecnologiaApi {
+ //sync dm data
+ @GET("api/sync")
+ suspend fun sync(@Query("since") since: Long): Response
+
@GET("api/health")
suspend fun checkHealth(): Response
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 8b94e29..bcb31aa 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,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,
+ @Json(name = "friends") val friends: List,
+ @Json(name = "full_snapshot") val fullSnapshot: Boolean,
+ @Json(name = "next_since") val nextSince: Long,
+ @Json(name = "rooms") val rooms: List
+)
+
+@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
diff --git a/app/src/main/java/com/example/login_v3/data/di/AppDatabase.kt b/app/src/main/java/com/example/login_v3/data/di/AppDatabase.kt
index 5fe81ab..22c6b9e 100644
--- a/app/src/main/java/com/example/login_v3/data/di/AppDatabase.kt
+++ b/app/src/main/java/com/example/login_v3/data/di/AppDatabase.kt
@@ -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
-}
\ No newline at end of file
+ abstract fun friendDao(): FriendDao
+ abstract fun roomLocalDao(): RoomLocalDao
+ abstract fun blockedUserDao(): BlockedUserDao
+}
diff --git a/app/src/main/java/com/example/login_v3/data/di/DatabaseModule.kt b/app/src/main/java/com/example/login_v3/data/di/DatabaseModule.kt
index 0e00fbe..e54ba23 100644
--- a/app/src/main/java/com/example/login_v3/data/di/DatabaseModule.kt
+++ b/app/src/main/java/com/example/login_v3/data/di/DatabaseModule.kt
@@ -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()
}
diff --git a/app/src/main/java/com/example/login_v3/data/di/TypeConverter.kt b/app/src/main/java/com/example/login_v3/data/di/TypeConverter.kt
index a3264d5..68b1b4f 100644
--- a/app/src/main/java/com/example/login_v3/data/di/TypeConverter.kt
+++ b/app/src/main/java/com/example/login_v3/data/di/TypeConverter.kt
@@ -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>(reactionListType)
+ private val reactionAdapter = moshi.adapter>(reactionListType)
+ private val partnerAdapter = moshi.adapter(Partner::class.java)
+ private val lastMessageAdapter = moshi.adapter(LastMessage::class.java)
@TypeConverter
fun fromReactionList(reactions: List?): String? {
- return adapter.toJson(reactions ?: emptyList())
+ return reactionAdapter.toJson(reactions ?: emptyList())
}
@TypeConverter
fun toReactionList(json: String?): List? {
if (json.isNullOrBlank()) return emptyList()
return try {
- adapter.fromJson(json)
+ reactionAdapter.fromJson(json)
} catch (e: Exception) {
emptyList()
}
}
-}
\ No newline at end of file
+
+ @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
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/login_v3/data/local/UserPreferences/UserPreferences_Repository.kt b/app/src/main/java/com/example/login_v3/data/local/UserPreferences/UserPreferences_Repository.kt
index 3507c9a..c082af6 100644
--- a/app/src/main/java/com/example/login_v3/data/local/UserPreferences/UserPreferences_Repository.kt
+++ b/app/src/main/java/com/example/login_v3/data/local/UserPreferences/UserPreferences_Repository.kt
@@ -21,6 +21,26 @@ class UserPreferencesRepository(private val dataStore: DataStore) {
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 = 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
+ }
}
// 讀取設定:直接在宣告時賦值,簡潔又安全
diff --git a/app/src/main/java/com/example/login_v3/data/local/dao/MessageDao.kt b/app/src/main/java/com/example/login_v3/data/local/dao/MessageDao.kt
new file mode 100644
index 0000000..382a8dc
--- /dev/null
+++ b/app/src/main/java/com/example/login_v3/data/local/dao/MessageDao.kt
@@ -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>
+
+ @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)
+
+ @Query("DELETE FROM messages WHERE id = :id")
+ suspend fun deleteById(id: String)
+
+ @Transaction
+ suspend fun replaceTempMessageWithSuccess(tempId: String, successEntity: MessageEntity) {
+ deleteById(tempId)
+ insertOrUpdate(successEntity)
+ }
+}
diff --git a/app/src/main/java/com/example/login_v3/data/local/dao/SyncDAOs.kt b/app/src/main/java/com/example/login_v3/data/local/dao/SyncDAOs.kt
new file mode 100644
index 0000000..b485334
--- /dev/null
+++ b/app/src/main/java/com/example/login_v3/data/local/dao/SyncDAOs.kt
@@ -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>
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertOrUpdate(friends: List)
+
+ @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>
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertOrUpdate(rooms: List)
+
+ @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>
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertOrUpdate(blocks: List)
+
+ @Query("DELETE FROM blocks WHERE userId = :userId")
+ suspend fun deleteById(userId: String)
+
+ @Query("DELETE FROM blocks")
+ suspend fun deleteAll()
+}
diff --git a/app/src/main/java/com/example/login_v3/data/local/entities/MessageEntity.kt b/app/src/main/java/com/example/login_v3/data/local/entities/MessageEntity.kt
new file mode 100644
index 0000000..1086026
--- /dev/null
+++ b/app/src/main/java/com/example/login_v3/data/local/entities/MessageEntity.kt
@@ -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?
+)
diff --git a/app/src/main/java/com/example/login_v3/data/local/entities/SyncEntities.kt b/app/src/main/java/com/example/login_v3/data/local/entities/SyncEntities.kt
new file mode 100644
index 0000000..ddd7fa4
--- /dev/null
+++ b/app/src/main/java/com/example/login_v3/data/local/entities/SyncEntities.kt
@@ -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
+)
diff --git a/app/src/main/java/com/example/login_v3/data/repository/basic/SyncRepository.kt b/app/src/main/java/com/example/login_v3/data/repository/basic/SyncRepository.kt
new file mode 100644
index 0000000..cb83022
--- /dev/null
+++ b/app/src/main/java/com/example/login_v3/data/repository/basic/SyncRepository.kt
@@ -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 {
+ 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)
+ }
+ }
+}
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 fa85b84..9ceefb1 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
@@ -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
diff --git a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging.kt b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging.kt
index 5f3b4e6..9879c46 100644
--- a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging.kt
+++ b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging.kt
@@ -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
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
deleted file mode 100644
index 8ec00bf..0000000
--- a/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Message_Cache.kt
+++ /dev/null
@@ -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?
-)
-
-@Dao
-interface MessageDao {
- // 1. 根據 roomId 監聽整個聊天室的訊息 Flow
- @Query("SELECT * FROM messages WHERE chatRoomId = :roomId ORDER BY createdAt DESC")
- fun getMessagesFlow(roomId: String): Flow>
-
- // 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)
-
- // 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
- }
-}
\ No newline at end of file