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:
parent
b6b3393999
commit
03f343578f
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue