MessageDetails VM observes either conversation or message in the DB

Depending on whether conversation mode is enabled or not. This is
particularly useful to have access to the model that's currently being
displayed to the user to perform actions.

MAILAND-1767
This commit is contained in:
Marino Meneghel 2021-05-20 09:35:04 +02:00
parent 8c781c36a6
commit f1d22547d6
6 changed files with 126 additions and 38 deletions

View File

@ -30,8 +30,11 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import ch.protonmail.android.activities.messageDetails.IntentExtrasData
import ch.protonmail.android.activities.messageDetails.MessagePrinter
@ -53,6 +56,7 @@ import ch.protonmail.android.data.local.model.Attachment
import ch.protonmail.android.data.local.model.Label
import ch.protonmail.android.data.local.model.Message
import ch.protonmail.android.data.local.model.PendingSend
import ch.protonmail.android.details.data.toDbModelList
import ch.protonmail.android.details.presentation.MessageDetailsActivity
import ch.protonmail.android.details.presentation.model.ConversationUiModel
import ch.protonmail.android.domain.entity.Id
@ -61,7 +65,6 @@ import ch.protonmail.android.events.DownloadEmbeddedImagesEvent
import ch.protonmail.android.events.Status
import ch.protonmail.android.jobs.helper.EmbeddedImage
import ch.protonmail.android.labels.domain.usecase.MoveMessagesToFolder
import ch.protonmail.android.mailbox.domain.Conversation
import ch.protonmail.android.mailbox.domain.ConversationsRepository
import ch.protonmail.android.mailbox.presentation.ConversationModeEnabled
import ch.protonmail.android.repository.MessageRepository
@ -79,6 +82,8 @@ import ch.protonmail.android.viewmodel.ConnectivityBaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
@ -99,6 +104,8 @@ import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
private const val STARRED_LABEL_ID = "10"
@HiltViewModel
internal class MessageDetailsViewModel @Inject constructor(
private val messageDetailsRepository: MessageDetailsRepository,
@ -135,26 +142,51 @@ internal class MessageDetailsViewModel @Inject constructor(
private val messageRenderer
by lazy { messageRendererFactory.create(viewModelScope, messageOrConversationId) }
private val messageFlow: Flow<Message?> =
userManager.primaryUserId
.flatMapLatest { userId ->
if (userId != null) {
messageRepository.findMessage(userId, messageOrConversationId)
} else {
emptyFlow()
val conversationUiModel: LiveData<ConversationUiModel>
get() {
return if (conversationModeEnabled(location)) {
getConversationLiveData().distinctUntilChanged().map { conversation ->
ConversationUiModel(
conversation.labels.any { it.id == STARRED_LABEL_ID },
conversation.subject,
conversation.labels.map { it.id },
conversation.messages?.toDbModelList().orEmpty()
)
}
} else {
getMessageLiveData().distinctUntilChanged().map {
ConversationUiModel(
it.isStarred ?: false,
it.subject,
it.labelIDsNotIncludingLocations,
listOf(it)
)
}
}
}
val message: LiveData<Message?> =
messageFlow.asLiveData(viewModelScope.coroutineContext)
val conversation: LiveData<Conversation?> =
conversationRepository.getConversation(messageOrConversationId, userManager.requireCurrentUserId()).map {
if (it is DataResult.Success) {
return@map it.value
private fun getMessageLiveData() = userManager.primaryUserId
.flatMapLatest { userId ->
if (userId != null) {
messageRepository.findMessage(userId, messageOrConversationId)
} else {
return@map null
emptyFlow()
}
}
.filterNotNull()
.asLiveData()
private fun getConversationLiveData() = userManager.primaryUserId
.flatMapLatest { userId ->
if (userId == null) {
return@flatMapLatest emptyFlow()
}
conversationRepository.getConversation(messageOrConversationId, Id(userId.id))
.filter { it is DataResult.Success }
.map {
return@map (it as DataResult.Success).value
}
}.asLiveData()
private var publicKeys: List<KeyInformation>? = null
@ -184,10 +216,10 @@ internal class MessageDetailsViewModel @Inject constructor(
}
val labels: Flow<List<Label>> =
messageFlow
.flatMapLatest { message ->
conversationUiModel.asFlow()
.flatMapLatest { conversation ->
val userId = UserId(userManager.requireCurrentUserId().s)
val labelsIds = (message?.labelIDsNotIncludingLocations ?: emptyList()).map(::Id)
val labelsIds = (conversation.labelIds).map(::Id)
labelRepository.findLabels(userId, labelsIds)
}
@ -201,10 +233,12 @@ internal class MessageDetailsViewModel @Inject constructor(
}
}
val messageAttachments: LiveData<List<Attachment>> by lazy {
val message = decryptedMessageData.value!!.messages.last()
messageDetailsRepository.findAttachments(message).distinctUntilChanged()
}
val messageAttachments: LiveData<List<Attachment>> =
conversationUiModel.switchMap {
val message = it.messages.last()
messageDetailsRepository.findAttachments(message).distinctUntilChanged()
}
val pendingSend: LiveData<PendingSend?> by lazy {
messageDetailsRepository.findPendingSendByOfflineMessageIdAsync(messageOrConversationId)
}
@ -326,7 +360,8 @@ internal class MessageDetailsViewModel @Inject constructor(
return ConversationUiModel(
message?.isStarred ?: false,
message?.subject ?: "",
messages.filterNotNull()
message?.labelIDsNotIncludingLocations.orEmpty(),
messages.filterNotNull(),
)
}
@ -360,7 +395,7 @@ internal class MessageDetailsViewModel @Inject constructor(
val attachmentMetadataList = attachmentMetadataDao.getAllAttachmentsForMessage(messageOrConversationId)
val embeddedImages = _embeddedImagesAttachments.mapNotNull {
attachmentsHelper.fromAttachmentToEmbeddedImage(
it, decryptedMessageData.value!!.messages.last().embeddedImageIds.toList()
it, conversationUiModel.value?.messages?.last()?.embeddedImageIds?.toList().orEmpty()
)
}
val embeddedImagesWithLocalFiles = mutableListOf<EmbeddedImage>()
@ -422,6 +457,7 @@ internal class MessageDetailsViewModel @Inject constructor(
_prepareEditMessageIntentResult.value = Event(intent)
}
}
fun viewOrDownloadAttachment(context: Context, attachmentToDownloadId: String, messageId: String) {
viewModelScope.launch(dispatchers.Io) {
@ -510,7 +546,8 @@ internal class MessageDetailsViewModel @Inject constructor(
}
fun prepareEmbeddedImages(): Boolean {
val message = decryptedMessageData.value?.messages?.last()
val message = conversationUiModel.value?.messages?.last()
message?.let {
val attachments = message.attachments
val embeddedImagesToFetch = ArrayList<EmbeddedImage>()
@ -534,7 +571,7 @@ internal class MessageDetailsViewModel @Inject constructor(
fun triggerVerificationKeyLoading() {
if (!fetchingPubKeys && publicKeys == null) {
val message = message.value
val message = conversationUiModel.value?.messages?.last()
message?.let {
fetchingPubKeys = true
viewModelScope.launch {
@ -547,7 +584,7 @@ internal class MessageDetailsViewModel @Inject constructor(
private suspend fun onFetchVerificationKeysEvent(pubKeys: List<KeyInformation>) {
Timber.v("FetchVerificationKeys received $pubKeys")
val message = message.value
val message = conversationUiModel.value?.messages?.last()
publicKeys = pubKeys
refreshedKeys = false
@ -563,14 +600,14 @@ internal class MessageDetailsViewModel @Inject constructor(
}
fun setAttachmentsList(attachments: List<Attachment>) {
val message = decryptedMessageData.value?.messages?.last()
message!!.setAttachmentList(attachments)
conversationUiModel.value?.messages?.last()?.setAttachmentList(attachments)
}
fun isPgpEncrypted(): Boolean = message.value?.messageEncryption?.isPGPEncrypted ?: false
fun isPgpEncrypted(): Boolean =
conversationUiModel.value?.messages?.last()?.messageEncryption?.isPGPEncrypted ?: false
fun printMessage(activityContext: Context) {
val message = message.value
val message = conversationUiModel.value?.messages?.last()
message?.let {
MessagePrinter(
activityContext,
@ -601,10 +638,11 @@ internal class MessageDetailsViewModel @Inject constructor(
}
fun moveToTrash() {
val message = conversationUiModel.value?.messages?.last()
moveMessagesToFolder(
listOf(messageOrConversationId),
Constants.MessageLocationType.TRASH.toString(),
message.value?.folderLocation ?: EMPTY_STRING
message?.folderLocation ?: EMPTY_STRING
)
}

View File

@ -20,7 +20,9 @@
package ch.protonmail.android.details.data
import ch.protonmail.android.data.local.model.Message
import ch.protonmail.android.data.local.model.MessageSender
import ch.protonmail.android.mailbox.data.recipientToCorespondent
import ch.protonmail.android.mailbox.data.toMessageRecipients
import ch.protonmail.android.mailbox.domain.model.Correspondent
import ch.protonmail.android.mailbox.domain.model.MessageDomainModel
@ -42,10 +44,30 @@ internal fun Message.toDomainModel() = MessageDomainModel(
labelsIds = allLabelIDs
)
internal fun MessageDomainModel.toDbModel() = Message(
messageId = id,
conversationId = conversationId,
subject = subject,
Unread = isUnread,
sender = MessageSender(sender.name, sender.address),
toList = receivers.toMessageRecipients(),
time = time,
numAttachments = attachmentsCount,
expirationTime = expirationTime,
isReplied = isReplied,
isRepliedAll = isRepliedAll,
isForwarded = isForwarded,
ccList = ccReceivers.toMessageRecipients(),
bccList = bccReceivers.toMessageRecipients(),
allLabelIDs = labelsIds
)
/**
* Converts a list of messages from db to a list of domain message model
*/
internal fun List<Message>.toDomainModelList() =
map { message -> message.toDomainModel() }
internal fun List<MessageDomainModel>.toDbModelList() =
map { message -> message.toDbModel() }

View File

@ -202,10 +202,7 @@ internal class MessageDetailsActivity : BaseStoragePermissionActivity() {
}
private fun continueSetup() {
viewModel.message.observe(this) { viewModel.loadMessageDetails() }
viewModel.conversation.observe(this) {
if (it != null) viewModel.loadMessageDetails()
}
viewModel.conversationUiModel.observe(this) { viewModel.loadMessageDetails() }
viewModel.decryptedMessageData.observe(this, DecryptedMessageObserver())
viewModel.labels

View File

@ -24,5 +24,6 @@ import ch.protonmail.android.data.local.model.Message
data class ConversationUiModel(
val isStarred: Boolean,
val subject: String?,
val labelIds: List<String>,
val messages: List<Message>
)

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.mailbox.data
import ch.protonmail.android.api.models.MessageRecipient
import ch.protonmail.android.data.local.model.MessageSender
import ch.protonmail.android.mailbox.domain.model.Correspondent
internal fun Correspondent.toMessageSender() = MessageSender(name, address)
internal fun Correspondent.toMessageRecipient() = MessageRecipient(name, address)
internal fun List<Correspondent>.toMessageRecipients() = map { it.toMessageRecipient() }

View File

@ -291,7 +291,7 @@ class MessageDetailsViewModelTest : ArchTest, CoroutinesTest {
// Then
val conversationUiModel = ConversationUiModel(
false, "subject4", messages = listOf(conversationMessage, conversationMessage)
false, "subject4", listOf("1", "2"), listOf(conversationMessage, conversationMessage)
)
assertEquals(conversationUiModel, conversationObserver.observedValues[0])
}