Message Details shows last message of a converations

when passing in a conversation ID. This is an intermediate step to get to show
all the messages of a conversation.

- MessageDetailsActivity receives "location" as input param (extra) and
  uses it to infer if conversation mode is ON
- loadMessageDetails loads conversation (when ON) and return the last
  message within the ConversationUiModel

MAILAND-1767
This commit is contained in:
Marino Meneghel 2021-05-18 18:47:25 +02:00
parent b6b3393999
commit 03f343578f
6 changed files with 231 additions and 19 deletions

View File

@ -134,6 +134,10 @@ public class SearchActivity extends BaseActivity {
} else {
Intent intent = AppUtil.decorInAppIntent(new Intent(SearchActivity.this, MessageDetailsActivity.class));
intent.putExtra(MessageDetailsActivity.EXTRA_MESSAGE_ID, mailboxUiItem.getItemId());
intent.putExtra(
MessageDetailsActivity.EXTRA_MESSAGE_LOCATION_ID,
MessageLocationType.SEARCH.getMessageLocationTypeValue()
);
startActivity(intent);
}
return null;

View File

@ -61,6 +61,8 @@ 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.ConversationsRepository
import ch.protonmail.android.mailbox.presentation.ConversationModeEnabled
import ch.protonmail.android.repository.MessageRepository
import ch.protonmail.android.ui.view.LabelChipUiModel
import ch.protonmail.android.usecase.VerifyConnection
@ -76,10 +78,12 @@ 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.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.proton.core.domain.arch.DataResult
import me.proton.core.domain.entity.UserId
import me.proton.core.util.kotlin.DispatcherProvider
import me.proton.core.util.kotlin.EMPTY_STRING
@ -108,6 +112,8 @@ internal class MessageDetailsViewModel @Inject constructor(
private val attachmentsHelper: AttachmentsHelper,
private val downloadUtils: DownloadUtils,
private val moveMessagesToFolder: MoveMessagesToFolder,
private val conversationModeEnabled: ConversationModeEnabled,
private val conversationRepository: ConversationsRepository,
savedStateHandle: SavedStateHandle,
messageRendererFactory: MessageRenderer.Factory,
verifyConnection: VerifyConnection,
@ -117,6 +123,13 @@ internal class MessageDetailsViewModel @Inject constructor(
private val messageId: String = savedStateHandle.get<String>(MessageDetailsActivity.EXTRA_MESSAGE_ID)
?: throw IllegalStateException("messageId in MessageDetails is Empty!")
private val location: Constants.MessageLocationType by lazy {
Constants.MessageLocationType.fromInt(
savedStateHandle.get<Int>(MessageDetailsActivity.EXTRA_MESSAGE_LOCATION_ID)
?: Constants.MessageLocationType.INVALID.messageLocationTypeValue
)
}
private val messageRenderer
by lazy { messageRendererFactory.create(viewModelScope, messageId) }
@ -242,24 +255,42 @@ internal class MessageDetailsViewModel @Inject constructor(
fun loadMessageDetails() {
viewModelScope.launch(dispatchers.Io) {
val userId = userManager.requireCurrentUserId()
val userId = Id(userManager.requireCurrentUserId().s)
val message = messageRepository.getMessage(userId, messageId, true)
if (message == null) {
Timber.d("Failed fetching Message Details for message $messageId")
_messageDetailsError.postValue(Event("Failed getting message details"))
if (conversationModeEnabled(location)) {
conversationRepository.getConversation(messageId, userId)
.map {
if (it is DataResult.Success) {
val conversation = it.value
val convMessage = conversation.messages?.get(0)!!
val message = messageRepository.getMessage(userId, convMessage.id, true)
onMessageLoaded(message)
} else if (it is DataResult.Error) {
onMessageLoaded(null)
}
}.first()
return@launch
}
val contactEmail = contactsRepository.findContactEmailByEmail(message.senderEmail)
message.senderDisplayName = contactEmail?.name.orEmpty()
refreshedKeys = true
decryptAndEmit(message)
val message = messageRepository.getMessage(userId, messageId, true)
onMessageLoaded(message)
}
}
private suspend fun onMessageLoaded(message: Message?) {
if (message == null) {
Timber.d("Failed fetching Message Details for message $messageId")
_messageDetailsError.postValue(Event("Failed getting message details"))
return
}
val contactEmail = contactsRepository.findContactEmailByEmail(message.senderEmail)
message.senderDisplayName = contactEmail?.name.orEmpty()
refreshedKeys = true
decryptAndEmit(message)
}
private suspend fun decryptAndEmit(message: Message) {
if (!message.isDownloaded) {
return

View File

@ -861,6 +861,7 @@ internal class MessageDetailsActivity : BaseStoragePermissionActivity() {
companion object {
const val EXTRA_MESSAGE_ID = "messageId"
const val EXTRA_MESSAGE_LOCATION_ID = "location"
const val EXTRA_MESSAGE_RECIPIENT_USER_ID = "message_recipient_user_id"
const val EXTRA_MESSAGE_RECIPIENT_USERNAME = "message_recipient_username"

View File

@ -1421,6 +1421,10 @@ class MailboxActivity :
)
)
intent.putExtra(MessageDetailsActivity.EXTRA_MESSAGE_ID, messageId)
intent.putExtra(
MessageDetailsActivity.EXTRA_MESSAGE_LOCATION_ID,
messageLocation?.messageLocationTypeValue
)
mailboxActivity?.startActivityForResult(intent, REQUEST_CODE_TRASH_MESSAGE_DETAILS)
}
}

View File

@ -351,6 +351,7 @@ class NotificationServer @Inject constructor(
// Create content Intent for open MessageDetailsActivity
val contentIntent = Intent(context, MessageDetailsActivity::class.java)
.putExtra(MessageDetailsActivity.EXTRA_MESSAGE_ID, messageId)
.putExtra(MessageDetailsActivity.EXTRA_MESSAGE_LOCATION_ID, message?.location)
.putExtra(MessageDetailsActivity.EXTRA_MESSAGE_RECIPIENT_USER_ID, user.id.s)
.putExtra(MessageDetailsActivity.EXTRA_MESSAGE_RECIPIENT_USERNAME, user.name.s)

View File

@ -25,7 +25,7 @@ import ch.protonmail.android.activities.messageDetails.repository.MessageDetails
import ch.protonmail.android.api.NetworkConfigurator
import ch.protonmail.android.attachments.AttachmentsHelper
import ch.protonmail.android.attachments.DownloadEmbeddedAttachmentsWorker
import ch.protonmail.android.core.Constants
import ch.protonmail.android.core.Constants.MessageLocationType.INBOX
import ch.protonmail.android.core.UserManager
import ch.protonmail.android.data.ContactsRepository
import ch.protonmail.android.data.LabelRepository
@ -33,30 +33,44 @@ import ch.protonmail.android.data.local.AttachmentMetadataDao
import ch.protonmail.android.data.local.model.ContactEmail
import ch.protonmail.android.data.local.model.Message
import ch.protonmail.android.data.local.model.MessageSender
import ch.protonmail.android.details.presentation.MessageDetailsActivity
import ch.protonmail.android.details.presentation.MessageDetailsActivity.Companion.EXTRA_MESSAGE_ID
import ch.protonmail.android.details.presentation.MessageDetailsActivity.Companion.EXTRA_MESSAGE_LOCATION_ID
import ch.protonmail.android.details.presentation.model.ConversationUiModel
import ch.protonmail.android.domain.entity.Id
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.domain.model.Correspondent
import ch.protonmail.android.mailbox.domain.model.LabelContext
import ch.protonmail.android.mailbox.domain.model.MessageDomainModel
import ch.protonmail.android.mailbox.presentation.ConversationModeEnabled
import ch.protonmail.android.repository.MessageRepository
import ch.protonmail.android.testAndroid.lifecycle.testObserver
import ch.protonmail.android.usecase.VerifyConnection
import ch.protonmail.android.usecase.fetch.FetchVerificationKeys
import ch.protonmail.android.utils.DownloadUtils
import io.mockk.Called
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runBlockingTest
import me.proton.core.domain.arch.DataResult
import me.proton.core.domain.arch.ResponseSource
import me.proton.core.test.android.ArchTest
import me.proton.core.test.kotlin.CoroutinesTest
import org.junit.Before
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals
private const val INPUT_ITEM_DETAIL_ID = "inputMessageOrConversationId"
class MessageDetailsViewModelTest : ArchTest, CoroutinesTest {
private val conversationRepository: ConversationsRepository = mockk(relaxed = true)
private val messageDetailsRepository: MessageDetailsRepository = mockk(relaxed = true)
private val messageRepository: MessageRepository = mockk(relaxed = true)
@ -79,10 +93,13 @@ class MessageDetailsViewModelTest : ArchTest, CoroutinesTest {
private val attachmentsWorker: DownloadEmbeddedAttachmentsWorker.Enqueuer = mockk(relaxed = true)
private val conversationModeEnabled: ConversationModeEnabled = mockk()
private val conversationModeEnabled: ConversationModeEnabled = mockk {
every { this@mockk.invoke(any()) } returns false
}
private val savedStateHandle = mockk<SavedStateHandle> {
every { get<String>(MessageDetailsActivity.EXTRA_MESSAGE_ID) } returns "mockMessageId"
every { get<String>(EXTRA_MESSAGE_ID) } returns INPUT_ITEM_DETAIL_ID
every { get<Int>(EXTRA_MESSAGE_LOCATION_ID) } returns INBOX.messageLocationTypeValue
}
private val userManager: UserManager = mockk(relaxed = true) {
@ -112,6 +129,8 @@ class MessageDetailsViewModelTest : ArchTest, CoroutinesTest {
attachmentsHelper,
DownloadUtils(),
moveMessagesToFolder,
conversationModeEnabled,
conversationRepository,
savedStateHandle,
messageRendererFactory,
verifyConnection,
@ -147,7 +166,7 @@ class MessageDetailsViewModelTest : ArchTest, CoroutinesTest {
viewModel.loadMessageDetails()
// Then
coVerify { messageRepository.getMessage(userId, "mockMessageId", true) }
coVerify { messageRepository.getMessage(userId, INPUT_ITEM_DETAIL_ID, true) }
}
@Test
@ -155,7 +174,7 @@ class MessageDetailsViewModelTest : ArchTest, CoroutinesTest {
// Given
val senderEmail = "senderEmail2"
val message = Message(
messageId = "mockMessageId",
messageId = INPUT_ITEM_DETAIL_ID,
isDownloaded = true,
sender = MessageSender("senderName", senderEmail)
)
@ -180,7 +199,7 @@ class MessageDetailsViewModelTest : ArchTest, CoroutinesTest {
// Given
val senderEmail = "senderEmail2"
val message = Message(
messageId = "mockMessageId",
messageId = INPUT_ITEM_DETAIL_ID,
isDownloaded = false,
sender = MessageSender("senderName", senderEmail)
)
@ -207,4 +226,156 @@ class MessageDetailsViewModelTest : ArchTest, CoroutinesTest {
assertEquals("Failed getting message details", messageErrorObserver.observedValues[0]?.getContentIfNotHandled())
}
@Test
fun loadMessageDetailsInvokesConversationRepositoryWithConversationIdAndUserIdWhenConversationModeIsEnabled() =
runBlockingTest {
// Given
val inputMessageLocation = INBOX
// messageId is defined as a field as it's needed at VM's instantiation time.
val inputConversationId = INPUT_ITEM_DETAIL_ID
val userId = Id("userId3")
every { userManager.requireCurrentUserId() } returns userId
coEvery { conversationModeEnabled(inputMessageLocation) } returns true
every { savedStateHandle.get<String>(EXTRA_MESSAGE_ID) } returns inputConversationId
every { savedStateHandle.get<Int>(EXTRA_MESSAGE_LOCATION_ID) } returns
inputMessageLocation.messageLocationTypeValue
// When
viewModel.loadMessageDetails()
// Then
coVerify(exactly = 0) { messageRepository.getMessage(any(), any()) }
coVerify { conversationRepository.getConversation(inputConversationId, userId) }
}
@Test
fun loadMessageDetailsEmitsConversationUiItemWithConversationDataWhenRepositoryReturnsAConversation() =
runBlockingTest {
// Given
val inputMessageLocation = INBOX
// messageId is defined as a field as it's needed at VM's instantiation time.
val inputConversationId = INPUT_ITEM_DETAIL_ID
val userId = Id("userId4")
val conversationObserver = viewModel.decryptedMessageData.testObserver()
val conversationId = UUID.randomUUID().toString()
val conversationMessage = Message(
messageId = "messageId4",
conversationId = conversationId,
subject = "subject4",
Unread = false,
sender = MessageSender("senderName", "sender@protonmail.ch"),
time = 82374724L,
numAttachments = 1,
expirationTime = 0L,
isReplied = false,
isRepliedAll = true,
isForwarded = false,
ccList = emptyList(),
bccList = emptyList(),
allLabelIDs = listOf("1", "2"),
isDownloaded = true
)
every { userManager.requireCurrentUserId() } returns userId
coEvery { conversationModeEnabled(inputMessageLocation) } returns true
every { savedStateHandle.get<String>(EXTRA_MESSAGE_ID) } returns inputConversationId
every { savedStateHandle.get<Int>(EXTRA_MESSAGE_LOCATION_ID) } returns
inputMessageLocation.messageLocationTypeValue
coEvery { conversationRepository.getConversation(inputConversationId, userId) } returns
flowOf(DataResult.Success(ResponseSource.Local, buildConversation(conversationId)))
coEvery { messageRepository.getMessage(userId, "messageId4", true) } returns conversationMessage
// When
viewModel.loadMessageDetails()
// Then
val conversationUiModel = ConversationUiModel(message = conversationMessage)
assertEquals(conversationUiModel, conversationObserver.observedValues[0])
}
@Test
fun loadMessageDetailsEmitsErrorWhenConversationIsEnabledAndConversationRepositorySucceedsButMessageIsNotInDatabase() =
runBlockingTest {
// Given
val inputMessageLocation = INBOX
// messageId is defined as a field as it's needed at VM's instantiation time.
val inputConversationId = INPUT_ITEM_DETAIL_ID
val userId = Id("userId4")
val errorObserver = viewModel.messageDetailsError.testObserver()
val conversationId = UUID.randomUUID().toString()
every { userManager.requireCurrentUserId() } returns userId
coEvery { conversationModeEnabled(inputMessageLocation) } returns true
every { savedStateHandle.get<String>(EXTRA_MESSAGE_ID) } returns inputConversationId
every { savedStateHandle.get<Int>(EXTRA_MESSAGE_LOCATION_ID) } returns
inputMessageLocation.messageLocationTypeValue
coEvery { conversationRepository.getConversation(inputConversationId, userId) } returns
flowOf(DataResult.Success(ResponseSource.Local, buildConversation(conversationId)))
coEvery { messageRepository.getMessage(userId, any(), true) } returns null
// When
viewModel.loadMessageDetails()
// Then
assertEquals("Failed getting message details", errorObserver.observedValues[0]?.getContentIfNotHandled())
}
@Test
fun loadMessageDetailsEmitsErrorWhenConversationIsEnabledAndConversationRepositoryFails() =
runBlockingTest {
// Given
val inputMessageLocation = INBOX
// messageId is defined as a field as it's needed at VM's instantiation time.
val inputConversationId = INPUT_ITEM_DETAIL_ID
val userId = Id("userId4")
val errorObserver = viewModel.messageDetailsError.testObserver()
val conversationId = UUID.randomUUID().toString()
every { userManager.requireCurrentUserId() } returns userId
coEvery { conversationModeEnabled(inputMessageLocation) } returns true
every { savedStateHandle.get<String>(EXTRA_MESSAGE_ID) } returns inputConversationId
every { savedStateHandle.get<Int>(EXTRA_MESSAGE_LOCATION_ID) } returns
inputMessageLocation.messageLocationTypeValue
coEvery { conversationRepository.getConversation(inputConversationId, userId) } returns
flowOf(DataResult.Error.Local("failed getting conversation", null))
// When
viewModel.loadMessageDetails()
// Then
assertEquals("Failed getting message details", errorObserver.observedValues[0]?.getContentIfNotHandled())
}
private fun buildConversation(conversationId: String): Conversation {
return Conversation(
conversationId,
"Conversation subject",
listOf(),
listOf(),
5,
2,
1,
0,
listOf(inboxLabelContext()),
listOf(
MessageDomainModel(
"messageId4",
conversationId,
"subject4",
false,
Correspondent("senderName", "sender@protonmail.ch"),
listOf(),
82374724L,
1,
0L,
isReplied = false,
isRepliedAll = true,
isForwarded = false,
ccReceivers = emptyList(),
bccReceivers = emptyList(),
labelsIds = listOf("1", "2")
)
)
)
}
private fun inboxLabelContext() = LabelContext(INBOX.messageLocationTypeValue.toString(), 0, 0, 0L, 0, 0)
}