package chats.presentation.chat_detail import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import chats.data.remote.signalr.ChatEvent import chats.data.remote.signalr.ChatHubClient import chats.domain.model.Message import chats.domain.repository.ChatRepository import chats.data.repository.toDomain import core.network.ServerConfig import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import java.io.File import javax.inject.Inject data class ChatDetailState( val messages: List = emptyList(), val isLoading: Boolean = false, val isTyping: Boolean = false, val typingUser: String? = null, val error: String? = null, val canCall: Boolean = true, val maxFileSize: Long = 100 * 1024 * 1024 ) @HiltViewModel class ChatDetailViewModel @Inject constructor( private val repository: ChatRepository, private val signalrClient: ChatHubClient, private val serverConfig: ServerConfig ) : ViewModel() { private val _state = MutableStateFlow(ChatDetailState()) val state: StateFlow = _state.asStateFlow() private var currentChatId: String? = null private var typingTimerJob: Job? = null private var lastTypingSentTime: Long = 0 init { val config = serverConfig.getServerConfig() _state.update { it.copy( canCall = config.features.calls, maxFileSize = config.limits.maxFileSize ) } } fun setChatId(chatId: String) { currentChatId = chatId loadMessages(chatId) observeSignalREvents() } fun loadMessages(chatId: String) { viewModelScope.launch { _state.update { it.copy(isLoading = true) } try { val messages = repository.getMessages(chatId) _state.update { it.copy(messages = messages, isLoading = false) } } catch (e: Exception) { _state.update { it.copy(isLoading = false, error = e.localizedMessage) } } } } private fun observeSignalREvents() { signalrClient.events .filter { event -> when(event) { is ChatEvent.NewMessage -> event.message.chatId == currentChatId is ChatEvent.ReactionUpdated -> event.chatId == currentChatId is ChatEvent.UserTyping -> event.chatId == currentChatId else -> false } } .onEach { event -> when (event) { is ChatEvent.NewMessage -> { // Avoid adding duplicates if already loaded _state.update { s -> val domainMsg = event.message.toDomain() if (s.messages.none { it.id == domainMsg.id }) { s.copy(messages = s.messages + domainMsg) } else s } } is ChatEvent.ReactionUpdated -> { updateMessageReaction(event.messageId, event.userId, event.emoji) } is ChatEvent.UserTyping -> { _state.update { it.copy(isTyping = true) } // Reset typing status after some delay would be better, // but usually server sends stopped_typing event. } else -> Unit } } .launchIn(viewModelScope) } private fun updateMessageReaction(messageId: String, userId: String, emoji: String) { _state.update { s -> val updatedMessages = s.messages.map { msg -> if (msg.id == messageId) { // Logic to update reactions map. // Note: Simplified logic, usually we need to know if it was added or removed. // If we assume reaction_updated is a toggle: val currentReactions = msg.reactions.toMutableMap() val count = currentReactions[emoji] ?: 0 // This is a placeholder logic as the exact behavior depends on server implementation. // For now, let's just increment/decrement based on some convention or just refresh. currentReactions[emoji] = count + 1 msg.copy(reactions = currentReactions) } else msg } s.copy(messages = updatedMessages) } } fun sendMessage(text: String) { val chatId = currentChatId ?: return viewModelScope.launch { try { repository.sendMessage(chatId, text) } catch (e: Exception) { _state.update { it.copy(error = e.localizedMessage) } } } } fun addReaction(messageId: String, emoji: String) { viewModelScope.launch { try { repository.addReaction(messageId, emoji) } catch (e: Exception) { // Ignore } } } fun uploadMedia(file: File) { if (file.length() > _state.value.maxFileSize) { _state.update { it.copy(error = "File too large") } return } // Upload logic... } }