add delta sync for dm-v2
This commit is contained in:
parent
6466a36547
commit
9df448b79d
6 changed files with 206 additions and 105 deletions
|
|
@ -2,7 +2,10 @@ package com.example.login_v3.data.di
|
|||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
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 dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -31,7 +34,24 @@ object DatabaseModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideMessageDao(database: AppDatabase): MessageDao {
|
||||
// 💡 告訴 Hilt:當有人要 MessageDao 時,就從 database 裡面把 messageDao() 拿出來提供給他!
|
||||
return database.messageDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFriendDao(database: AppDatabase): FriendDao {
|
||||
return database.friendDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRoomLocalDao(database: AppDatabase): RoomLocalDao {
|
||||
return database.roomLocalDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBlockedUserDao(database: AppDatabase): BlockedUserDao {
|
||||
return database.blockedUserDao()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,13 +7,31 @@ import com.example.login_v3.data.api.api_class.Friend
|
|||
import com.example.login_v3.data.api.api_class.PendingFriendApiModel
|
||||
import com.example.login_v3.data.api.api_class.UserDetail
|
||||
import com.example.login_v3.data.api.api_class.UserSearchResponse
|
||||
import com.example.login_v3.data.local.dao.FriendDao
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import retrofit2.Response
|
||||
import javax.inject.Inject
|
||||
|
||||
class FriendsRepository @Inject constructor(
|
||||
private val apiService: TecnologiaApi
|
||||
private val apiService: TecnologiaApi,
|
||||
private val friendDao: FriendDao
|
||||
) {
|
||||
|
||||
// 🌟 核心:監聽本地資料庫的好友列表 Flow
|
||||
val friendsFlow: Flow<List<Friend>> = friendDao.getAllFriendsFlow().map { entities ->
|
||||
entities.map { entity ->
|
||||
Friend(
|
||||
friendId = entity.userId,
|
||||
username = entity.username,
|
||||
displayName = entity.displayName,
|
||||
avatarUrl = entity.avatarUrl,
|
||||
status = entity.status,
|
||||
acceptedAt = null // Sync API 目前沒給這個,先設為 null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getFriendList(): Result<List<Friend>> {
|
||||
return try {
|
||||
val response = apiService.getFriends()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.example.login_v3.data.repository.basic
|
||||
|
||||
import android.util.Log
|
||||
import androidx.room.withTransaction
|
||||
import com.example.login_v3.data.api.TecnologiaApi
|
||||
import com.example.login_v3.data.di.AppDatabase
|
||||
|
|
@ -8,6 +9,8 @@ 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 kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -21,22 +24,38 @@ class SyncRepository @Inject constructor(
|
|||
private val roomDao = db.roomLocalDao()
|
||||
private val blockDao = db.blockedUserDao()
|
||||
|
||||
// 🔒 使用 Mutex 確保同一時間只有一個同步任務在執行,避免重複請求
|
||||
private val syncMutex = Mutex()
|
||||
|
||||
suspend fun performSync(): Result<Unit> {
|
||||
return try {
|
||||
// 如果已經有同步在跑,直接回傳成功(避免重複排隊)
|
||||
if (syncMutex.isLocked) {
|
||||
Log.d("SyncDebug", "⏳ [Sync] 偵測到已有同步任務正在進行,跳過本次請求")
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
return syncMutex.withLock {
|
||||
try {
|
||||
val since = userPrefs.syncSinceFlow.first()
|
||||
Log.d("SyncDebug", "🚀 [Sync] 開始同步 | since: $since")
|
||||
|
||||
val response = api.sync(since)
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body() ?: return Result.failure(Exception("Sync body is null"))
|
||||
|
||||
Log.d("SyncDebug", "✅ [Sync] API 成功 | FullSnapshot: ${body.fullSnapshot} | nextSince: ${body.nextSince}")
|
||||
Log.d("SyncDebug", "📦 [Sync] 收到: Blocks(${body.blocks.size}), Friends(${body.friends.size}), Rooms(${body.rooms.size})")
|
||||
|
||||
db.withTransaction {
|
||||
if (body.fullSnapshot) {
|
||||
Log.w("SyncDebug", "⚠️ [Sync] 收到 FullSnapshot,正在清空本地舊資料...")
|
||||
friendDao.deleteAll()
|
||||
roomDao.deleteAll()
|
||||
blockDao.deleteAll()
|
||||
}
|
||||
|
||||
// Process Blocks
|
||||
// 1. 處理黑名單
|
||||
body.blocks.forEach { block ->
|
||||
if (block.removed) {
|
||||
blockDao.deleteById(block.userId)
|
||||
|
|
@ -53,7 +72,7 @@ class SyncRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// Process Friends
|
||||
// 2. 處理好友
|
||||
body.friends.forEach { friend ->
|
||||
if (friend.removed) {
|
||||
friendDao.deleteById(friend.userId)
|
||||
|
|
@ -71,7 +90,7 @@ class SyncRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// Process Rooms
|
||||
// 3. 處理聊天室
|
||||
body.rooms.forEach { room ->
|
||||
if (room.removed) {
|
||||
roomDao.deleteById(room.roomId)
|
||||
|
|
@ -93,13 +112,32 @@ class SyncRepository @Inject constructor(
|
|||
}
|
||||
|
||||
userPrefs.saveSyncSince(body.nextSince)
|
||||
Log.d("SyncDebug", "💾 [Sync] 同步完成,已更新 nextSince 為 ${body.nextSince}")
|
||||
}
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Log.e("SyncDebug", "❌ [Sync] API 失敗 | Code: ${response.code()}")
|
||||
Result.failure(Exception("Sync API failed: ${response.code()}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("SyncDebug", "❌ [Sync] 發生異常", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🛠️ 調試專用:重置同步狀態並清空本地資料
|
||||
* 呼叫此方法後,下一次同步將會從 since=0 開始抓取完整資料
|
||||
*/
|
||||
suspend fun resetSyncAndClearData() {
|
||||
Log.w("SyncDebug", "🧨 [Debug] 正在重置同步狀態並清空資料庫...")
|
||||
db.withTransaction {
|
||||
friendDao.deleteAll()
|
||||
roomDao.deleteAll()
|
||||
blockDao.deleteAll()
|
||||
userPrefs.saveSyncSince(0L)
|
||||
}
|
||||
Log.d("SyncDebug", "✅ [Debug] 重置完成")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ 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.data.local.dao.MessageDao
|
||||
import com.example.login_v3.data.local.dao.RoomLocalDao
|
||||
import com.example.login_v3.data.local.entities.MessageEntity
|
||||
import com.example.login_v3.data.local.entities.MessageStatus
|
||||
import com.example.login_v3.data.local.entities.RoomLocalEntity
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
|
|
@ -52,12 +54,31 @@ import kotlin.jvm.java
|
|||
class ChatRoomsRepository @Inject constructor(
|
||||
private val api: TecnologiaApi,
|
||||
private val messageDao: MessageDao,
|
||||
private val roomLocalDao: RoomLocalDao, // 👈 新增:注入房間 DAO
|
||||
private val webSocketManager: ChatWebSocketManager, // 👈 1. 注入 Socket 管理器
|
||||
private val moshi: Moshi, // 👈 用於解析 Socket 傳來的 JSON
|
||||
private val tokenManager: TokenManager,
|
||||
@ApplicationContext private val context: Context // 注入 ApplicationContext 用於處理 Uri 檔案
|
||||
) {
|
||||
|
||||
// 🌟 核心:監聽本地資料庫的聊天室列表 Flow
|
||||
val chatRoomsFlow: Flow<List<ChatRoom>> = roomLocalDao.getAllRoomsFlow().map { entities ->
|
||||
entities.map { entity ->
|
||||
ChatRoom(
|
||||
roomId = entity.roomId,
|
||||
roomName = entity.nickname, // 或者 entity.partner?.displayName ? 根據 DTO 結構映射
|
||||
roomType = null, // Entity 暫時沒存,可視需求補上
|
||||
roomIconUrl = null, // Entity 暫時沒存,可視需求補上
|
||||
isMuted = entity.isMuted,
|
||||
isPinned = entity.isPinned,
|
||||
unreadCount = entity.unreadCount,
|
||||
mentionCount = entity.mentionCount,
|
||||
partner = entity.partner,
|
||||
lastMessage = entity.lastMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 建立一個跟著 Repository 生命週期走的 Scope
|
||||
private val repositoryScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.example.login_v3.data.api.api_class.Friend
|
|||
import com.example.login_v3.data.api.api_class.PendingFriendApiModel
|
||||
import com.example.login_v3.data.api.api_class.UserDetail
|
||||
import com.example.login_v3.data.repository.basic.FriendsRepository
|
||||
import com.example.login_v3.data.repository.basic.SyncRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -31,32 +32,42 @@ data class ContactUiState(
|
|||
)
|
||||
@HiltViewModel
|
||||
class ContactListViewModel @Inject constructor(
|
||||
private val repository: FriendsRepository
|
||||
private val repository: FriendsRepository,
|
||||
private val syncRepository: SyncRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(ContactUiState())
|
||||
val uiState: StateFlow<ContactUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
// 1. 監聽本地好友 Flow,一旦資料庫變動,UI 自動刷新
|
||||
observeFriends()
|
||||
// 2. 啟動時跑一次同步
|
||||
refreshAll()
|
||||
}
|
||||
|
||||
private fun observeFriends() {
|
||||
viewModelScope.launch {
|
||||
repository.friendsFlow.collect { friends ->
|
||||
_uiState.update { it.copy(friends = friends) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshAll() {
|
||||
viewModelScope.launch {
|
||||
// 1. 開始 Loading 並清除舊錯誤
|
||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||
|
||||
// 2. 使用 zip 或單獨 fetch,這裡示範簡單的循序或並行處理
|
||||
val friendsResult = repository.getFriendList()
|
||||
// 🎯 同步核心:跑 Delta-Sync
|
||||
val syncResult = syncRepository.performSync()
|
||||
// 同時還是要抓一下 Pending Request (目前 Sync API 沒給這塊)
|
||||
val pendingResult = repository.getPendingRequests()
|
||||
|
||||
// 3. 統一處理結果
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
friends = friendsResult.getOrDefault(state.friends),
|
||||
pendingRequests = pendingResult.getOrDefault(state.pendingRequests),
|
||||
error = (friendsResult.exceptionOrNull() ?: pendingResult.exceptionOrNull())?.message
|
||||
error = (syncResult.exceptionOrNull() ?: pendingResult.exceptionOrNull())?.message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,49 +9,42 @@ import com.example.login_v3.R
|
|||
import com.example.login_v3.data.api.api_class.ChatRoom
|
||||
import com.example.login_v3.data.api.api_class.Message
|
||||
import com.example.login_v3.data.repository.dm.ChatRoomsRepository
|
||||
import com.example.login_v3.data.repository.basic.SyncRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@HiltViewModel
|
||||
class ChatRoomsViewModel @Inject constructor(
|
||||
private val repository: ChatRoomsRepository
|
||||
private val repository: ChatRoomsRepository,
|
||||
private val syncRepository: SyncRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _roomsState = MutableStateFlow<List<ChatRoom>>(emptyList())
|
||||
val roomsState: StateFlow<List<ChatRoom>> = _roomsState
|
||||
|
||||
init {
|
||||
observeRooms()
|
||||
loadRooms()
|
||||
}
|
||||
|
||||
private fun observeRooms() {
|
||||
viewModelScope.launch {
|
||||
repository.chatRoomsFlow.collectLatest { rooms ->
|
||||
_roomsState.value = rooms
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadRooms() {
|
||||
viewModelScope.launch {
|
||||
Log.d("ChatRoomsDebug", "1. 開始呼叫 loadRooms()...")
|
||||
val result = repository.fetchRooms()
|
||||
|
||||
result.onSuccess { response ->
|
||||
val roomsList = response.rooms
|
||||
Log.d("ChatRoomsDebug", "2. API 請求成功!拿到 ${roomsList?.size ?: 0} 個聊天室")
|
||||
|
||||
if (roomsList.isNullOrEmpty()) {
|
||||
Log.w("ChatRoomsDebug", "⚠️ 注意:後端回傳的 rooms 列表是空的(或為 null)!")
|
||||
} else {
|
||||
roomsList.forEach { room ->
|
||||
Log.d("ChatRoomsDebug", "-> 房間 ID: ${room.roomId}, 名稱: ${room.roomName}, Partner: ${room.partner?.displayName}, 狀態: ${room.partner?.status}")
|
||||
}
|
||||
}
|
||||
|
||||
// 更新狀態
|
||||
_roomsState.value = roomsList ?: emptyList()
|
||||
Log.d("ChatRoomsDebug", "3. _roomsState 已更新,當前長度: ${_roomsState.value.size}")
|
||||
|
||||
}.onFailure { error ->
|
||||
// ⭐ 這裡非常重要,一定要印出 stackTrace 才知道為什麼失敗(例如:JSON 解析失敗、404、網路斷線)
|
||||
Log.e("ChatRoomsDebug", "❌ API 請求失敗 (onFailure)!!", error)
|
||||
Log.d("ChatRoomsDebug", "📡 [ChatRoomsViewModel] 觸發同步...")
|
||||
val result = syncRepository.performSync()
|
||||
result.onFailure { error ->
|
||||
Log.e("ChatRoomsDebug", "❌ 同步失敗", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue