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 {
|
interface TecnologiaApi {
|
||||||
|
|
||||||
|
//sync dm data
|
||||||
|
@GET("api/sync")
|
||||||
|
suspend fun sync(@Query("since") since: Long): Response<SyncResponse>
|
||||||
|
|
||||||
@GET("api/health")
|
@GET("api/health")
|
||||||
suspend fun checkHealth(): Response<Unit>
|
suspend fun checkHealth(): Response<Unit>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +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.example.login_v3.data.local.entities.MessageStatus
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.squareup.moshi.Moshi
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
data class CallLogInfo(
|
data class CallLogInfo(
|
||||||
@Json(name = "type") val type: String, // 例如: call_missed, call_rejected
|
@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.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.example.login_v3.home.Message.ViewModel.Detail.MessageDao
|
import com.example.login_v3.data.local.dao.BlockedUserDao
|
||||||
import com.example.login_v3.home.Message.ViewModel.Detail.MessageEntity
|
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)
|
@TypeConverters(RoomConverters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun messageDao(): MessageDao
|
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 android.content.Context
|
||||||
import androidx.room.Room
|
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.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -24,7 +24,7 @@ object DatabaseModule {
|
||||||
AppDatabase::class.java,
|
AppDatabase::class.java,
|
||||||
"tecnologia_chat_db" // 資料庫檔案名稱
|
"tecnologia_chat_db" // 資料庫檔案名稱
|
||||||
)
|
)
|
||||||
// .fallbackToDestructiveMigration() // 💡 提示:未來如果修改 Entity 欄位導致崩潰,可以先取消這行註解來清除舊資料庫
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,62 @@ import androidx.room.TypeConverter
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory // 👈 確保有 import 這個
|
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
|
import com.example.login_v3.data.api.api_class.Reaction
|
||||||
|
|
||||||
class RoomConverters {
|
class RoomConverters {
|
||||||
// 💡 核心修正:加入 KotlinJsonAdapterFactory 賦予 Moshi 反射辨識 Kotlin Data Class 的能力
|
|
||||||
private val moshi = Moshi.Builder()
|
private val moshi = Moshi.Builder()
|
||||||
.addLast(KotlinJsonAdapterFactory())
|
.addLast(KotlinJsonAdapterFactory())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val reactionListType = Types.newParameterizedType(List::class.java, Reaction::class.java)
|
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
|
@TypeConverter
|
||||||
fun fromReactionList(reactions: List<Reaction>?): String? {
|
fun fromReactionList(reactions: List<Reaction>?): String? {
|
||||||
return adapter.toJson(reactions ?: emptyList())
|
return reactionAdapter.toJson(reactions ?: emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun toReactionList(json: String?): List<Reaction>? {
|
fun toReactionList(json: String?): List<Reaction>? {
|
||||||
if (json.isNullOrBlank()) return emptyList()
|
if (json.isNullOrBlank()) return emptyList()
|
||||||
return try {
|
return try {
|
||||||
adapter.fromJson(json)
|
reactionAdapter.fromJson(json)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emptyList()
|
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")
|
val THEME_MODE = stringPreferencesKey("theme_mode")
|
||||||
// 新增:儲存自訂壁紙檔案路徑的 Key
|
// 新增:儲存自訂壁紙檔案路徑的 Key
|
||||||
val CUSTOM_WALLPAPER_PATH = stringPreferencesKey("custom_wallpaper_path")
|
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.api.api_class.WebSocketEventResponse
|
||||||
import com.example.login_v3.data.di.ChatWebSocketManager
|
import com.example.login_v3.data.di.ChatWebSocketManager
|
||||||
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.Detail.MessageDao
|
import com.example.login_v3.data.local.dao.MessageDao
|
||||||
import com.example.login_v3.home.Message.ViewModel.Detail.MessageEntity
|
import com.example.login_v3.data.local.entities.MessageEntity
|
||||||
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.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.squareup.moshi.Moshi
|
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.UI.Detail.Message_Component.UserAvatar
|
||||||
import com.example.login_v3.home.Message.ViewModel.Detail.ChatViewModel
|
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.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.MessagesUiState
|
||||||
import com.example.login_v3.home.Message.ViewModel.Detail.SendMessageState
|
import com.example.login_v3.home.Message.ViewModel.Detail.SendMessageState
|
||||||
import com.example.login_v3.navigation.BottomBarViewModel
|
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