package org.thoughtcrime.securesms.conversation.v2 import android.content.ContentResolver import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import app.cash.copper.flow.observeQuery import com.goterl.lazysodium.utils.KeyPair import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.repository.ConversationRepository import java.util.UUID class ConversationViewModel( val threadId: Long, val edKeyPair: KeyPair?, private val contentResolver: ContentResolver, private val repository: ConversationRepository, private val storage: Storage ) : ViewModel() { val showSendAfterApprovalText: Boolean get() = recipient?.run { isContactRecipient && !isLocalNumber && !hasApprovedMe() } ?: false private val _uiState = MutableStateFlow(ConversationUiState(conversationExists = true)) val uiState: StateFlow = _uiState private var _recipient: RetrieveOnce = RetrieveOnce { repository.maybeGetRecipientForThreadId(threadId) } val recipient: Recipient? get() = _recipient.value private var _openGroup: RetrieveOnce = RetrieveOnce { storage.getOpenGroup(threadId) } val openGroup: OpenGroup? get() = _openGroup.value val serverCapabilities: List get() = openGroup?.let { storage.getServerCapabilities(it.server) } ?: listOf() val blindedPublicKey: String? get() = if (openGroup == null || edKeyPair == null || !serverCapabilities.contains(OpenGroupApi.Capability.BLIND.name.lowercase())) null else { SodiumUtilities.blindedKeyPair(openGroup!!.publicKey, edKeyPair)?.publicKey?.asBytes ?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString } init { viewModelScope.launch(Dispatchers.IO) { contentResolver.observeQuery(DatabaseContentProviders.Conversation.getUriForThread(threadId)) .collect { val recipientExists = storage.getRecipientForThread(threadId) != null if (!recipientExists && _uiState.value.conversationExists) { _uiState.update { it.copy(conversationExists = false) } } } } } fun saveDraft(text: String) { GlobalScope.launch(Dispatchers.IO) { repository.saveDraft(threadId, text) } } fun getDraft(): String? { val draft: String? = repository.getDraft(threadId) viewModelScope.launch(Dispatchers.IO) { repository.clearDrafts(threadId) } return draft } fun inviteContacts(contacts: List) { repository.inviteContacts(threadId, contacts) } fun block() { val recipient = recipient ?: return Log.w("Loki", "Recipient was null for block action") if (recipient.isContactRecipient) { repository.setBlocked(recipient, true) } } fun unblock() { val recipient = recipient ?: return Log.w("Loki", "Recipient was null for unblock action") if (recipient.isContactRecipient) { repository.setBlocked(recipient, false) } } fun deleteThread() = viewModelScope.launch { repository.deleteThread(threadId) } fun deleteLocally(message: MessageRecord) { val recipient = recipient ?: return Log.w("Loki", "Recipient was null for delete locally action") repository.deleteLocally(recipient, message) } fun setRecipientApproved() { val recipient = recipient ?: return Log.w("Loki", "Recipient was null for set approved action") repository.setApproved(recipient, true) } fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch { val recipient = recipient ?: return@launch repository.deleteForEveryone(threadId, recipient, message) .onFailure { showMessage("Couldn't delete message due to error: $it") } } fun deleteMessagesWithoutUnsendRequest(messages: Set) = viewModelScope.launch { repository.deleteMessageWithoutUnsendRequest(threadId, messages) .onFailure { showMessage("Couldn't delete message due to error: $it") } } fun banUser(recipient: Recipient) = viewModelScope.launch { repository.banUser(threadId, recipient) .onSuccess { showMessage("Successfully banned user") } .onFailure { showMessage("Couldn't ban user due to error: $it") } } fun banAndDeleteAll(recipient: Recipient) = viewModelScope.launch { repository.banAndDeleteAll(threadId, recipient) .onSuccess { showMessage("Successfully banned user and deleted all their messages") } .onFailure { showMessage("Couldn't execute request due to error: $it") } } fun acceptMessageRequest() = viewModelScope.launch { val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for accept message request action") repository.acceptMessageRequest(threadId, recipient) .onSuccess { _uiState.update { it.copy(isMessageRequestAccepted = true) } } .onFailure { showMessage("Couldn't accept message request due to error: $it") } } fun declineMessageRequest() { repository.declineMessageRequest(threadId) } private fun showMessage(message: String) { _uiState.update { currentUiState -> val messages = currentUiState.uiMessages + UiMessage( id = UUID.randomUUID().mostSignificantBits, message = message ) currentUiState.copy(uiMessages = messages) } } fun messageShown(messageId: Long) { _uiState.update { currentUiState -> val messages = currentUiState.uiMessages.filterNot { it.id == messageId } currentUiState.copy(uiMessages = messages) } } fun hasReceived(): Boolean { return repository.hasReceived(threadId) } fun updateRecipient() { _recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId)) } @dagger.assisted.AssistedFactory interface AssistedFactory { fun create(threadId: Long, edKeyPair: KeyPair?, contentResolver: ContentResolver): Factory } @Suppress("UNCHECKED_CAST") class Factory @AssistedInject constructor( @Assisted private val threadId: Long, @Assisted private val edKeyPair: KeyPair?, @Assisted private val contentResolver: ContentResolver, private val repository: ConversationRepository, private val storage: Storage ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return ConversationViewModel(threadId, edKeyPair, contentResolver, repository, storage) as T } } } data class UiMessage(val id: Long, val message: String) data class ConversationUiState( val uiMessages: List = emptyList(), val isMessageRequestAccepted: Boolean? = null, val conversationExists: Boolean ) data class RetrieveOnce(val retrieval: () -> T?) { private var triedToRetrieve: Boolean = false private var _value: T? = null val value: T? get() { if (triedToRetrieve) { return _value } triedToRetrieve = true _value = retrieval() return _value } fun updateTo(value: T?) { _value = value } }