152 lines
5.4 KiB
Kotlin
152 lines
5.4 KiB
Kotlin
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<Message> = 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<ChatDetailState> = _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...
|
|
}
|
|
}
|