184 lines
6.8 KiB
Kotlin
184 lines
6.8 KiB
Kotlin
package chats.presentation.chat_list
|
||
|
||
import androidx.lifecycle.ViewModel
|
||
import androidx.lifecycle.viewModelScope
|
||
import chats.domain.model.Chat
|
||
import chats.domain.repository.ChatRepository
|
||
import chats.data.remote.signalr.ChatHubClient
|
||
import chats.data.remote.signalr.ChatEvent
|
||
import core.network.ServerConfig
|
||
import core.security.TokenManager
|
||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||
import kotlinx.coroutines.flow.*
|
||
import kotlinx.coroutines.launch
|
||
import javax.inject.Inject
|
||
import chats.data.repository.toDomain
|
||
|
||
data class ChatListState(
|
||
val chats: List<Chat> = emptyList(),
|
||
val isLoading: Boolean = false,
|
||
val error: String? = null,
|
||
val isStoriesEnabled: Boolean = true
|
||
)
|
||
|
||
@HiltViewModel
|
||
class ChatListViewModel @Inject constructor(
|
||
private val repository: ChatRepository,
|
||
private val authRepository: auth.domain.repository.AuthRepository,
|
||
private val signalrClient: ChatHubClient,
|
||
private val serverConfig: ServerConfig,
|
||
private val tokenManager: TokenManager
|
||
) : ViewModel() {
|
||
|
||
private val _state = MutableStateFlow(ChatListState())
|
||
val state: StateFlow<ChatListState> = _state.asStateFlow()
|
||
|
||
init {
|
||
updatePushToken()
|
||
|
||
val isStoriesEnabled = try {
|
||
serverConfig.getServerConfig().features.stories
|
||
} catch (e: Exception) {
|
||
true
|
||
}
|
||
_state.update { it.copy(isStoriesEnabled = isStoriesEnabled) }
|
||
|
||
val token = tokenManager.getToken()
|
||
val baseUrl = serverConfig.getBaseUrl()
|
||
|
||
// Подключаемся к SignalR только если есть токен И введён URL сервера
|
||
if (token != null && baseUrl.isNotBlank()) {
|
||
signalrClient.connect(baseUrl.removeSuffix("/api/"), token)
|
||
}
|
||
|
||
// Single Source of Truth - подписываемся на Flow из Room
|
||
observeChatsFromLocal()
|
||
|
||
// Запускаем периодическую синхронизацию
|
||
viewModelScope.launch {
|
||
repository.schedulePeriodicSync()
|
||
}
|
||
|
||
observeSignalREvents()
|
||
}
|
||
|
||
/**
|
||
* Подписка на локальные чаты из Room (Single Source of Truth)
|
||
*/
|
||
private fun observeChatsFromLocal() {
|
||
repository.getChatsFlow()
|
||
.onEach { chats ->
|
||
_state.update { currentState ->
|
||
currentState.copy(
|
||
chats = sortChats(chats),
|
||
isLoading = false
|
||
)
|
||
}
|
||
}
|
||
.catch { e ->
|
||
android.util.Log.e("ChatListVM", "Error observing chats", e)
|
||
_state.update { it.copy(isLoading = false, error = e.message) }
|
||
}
|
||
.launchIn(viewModelScope)
|
||
}
|
||
|
||
/**
|
||
* Принудительная синхронизация чатов с сервером
|
||
*/
|
||
fun loadChats() {
|
||
viewModelScope.launch {
|
||
_state.update { it.copy(isLoading = true) }
|
||
try {
|
||
repository.syncChats()
|
||
} catch (e: Exception) {
|
||
_state.update { it.copy(isLoading = false, error = e.message) }
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun updatePushToken() {
|
||
com.google.firebase.messaging.FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
|
||
if (task.isSuccessful) {
|
||
val token = task.result
|
||
viewModelScope.launch {
|
||
try {
|
||
authRepository.updatePushToken(token)
|
||
} catch (e: Exception) {
|
||
android.util.Log.e("ChatListVM", "Failed to update push token: ${e.message}")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun getCurrentUserId(): String = tokenManager.getUserId() ?: ""
|
||
|
||
private fun sortChats(chats: List<Chat>): List<Chat> {
|
||
return chats.sortedWith(compareByDescending<Chat> {
|
||
it.name.equals("Избранное", ignoreCase = true) || it.name.equals("Saved Messages", ignoreCase = true)
|
||
}.thenByDescending { it.lastMessage?.createdAt })
|
||
}
|
||
|
||
private fun observeSignalREvents() {
|
||
signalrClient.events
|
||
.onEach { event ->
|
||
when (event) {
|
||
is ChatEvent.NewMessage -> {
|
||
updateChatsWithNewMessage(event)
|
||
}
|
||
is ChatEvent.NewChat -> {
|
||
val currentUserId = getCurrentUserId()
|
||
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
||
_state.update { it.copy(chats = listOf(event.chat.toDomain(currentUserId, baseUrl)) + it.chats) }
|
||
}
|
||
is ChatEvent.MessagesRead -> {
|
||
if (event.userId == getCurrentUserId()) {
|
||
_state.update { currentState ->
|
||
val updatedChats = currentState.chats.map { chat ->
|
||
if (chat.id == event.chatId) {
|
||
chat.copy(unreadCount = 0)
|
||
} else chat
|
||
}
|
||
currentState.copy(chats = sortChats(updatedChats))
|
||
}
|
||
}
|
||
}
|
||
else -> Unit
|
||
}
|
||
}
|
||
.launchIn(viewModelScope)
|
||
}
|
||
|
||
private fun updateChatsWithNewMessage(event: ChatEvent.NewMessage) {
|
||
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
||
val currentUserId = getCurrentUserId()
|
||
|
||
_state.update { currentState ->
|
||
val updatedChats = currentState.chats.map { chat ->
|
||
if (chat.id.equals(event.message.chatId, ignoreCase = true)) {
|
||
val isMyMessage = event.message.senderId == currentUserId
|
||
val isAlreadySeen = chat.lastMessage?.id == event.message.id
|
||
|
||
val lastMsgDomain = event.message.toDomain(currentUserId, baseUrl)
|
||
val newCount = if (isMyMessage || isAlreadySeen) {
|
||
chat.unreadCount
|
||
} else {
|
||
chat.unreadCount + 1
|
||
}
|
||
|
||
if (!isAlreadySeen) {
|
||
android.util.Log.d("ChatListVM", "Message ${event.message.id} -> Count ${chat.unreadCount} -> $newCount")
|
||
}
|
||
|
||
chat.copy(
|
||
lastMessage = lastMsgDomain,
|
||
unreadCount = newCount
|
||
)
|
||
} else chat
|
||
}
|
||
|
||
currentState.copy(chats = sortChats(updatedChats))
|
||
}
|
||
}
|
||
}
|