Add Recipients to StoreDraftWithAllFields

This commit is contained in:
Adam Jodlowski
2023-07-28 13:15:51 +02:00
parent 1faa61389f
commit 001a2d33db
8 changed files with 325 additions and 17 deletions
@@ -21,12 +21,19 @@ package ch.protonmail.android.mailcomposer.domain.model
data class DraftFields(
val sender: SenderEmail,
val subject: Subject,
val body: DraftBody
val body: DraftBody,
val recipientsTo: RecipientsTo,
val recipientsCc: RecipientsCc,
val recipientsBcc: RecipientsBcc
) {
/**
* Returns true if all of the fields (except sender) are blank.
* Can be used to infer whether these fields should be used to store a draft or discarded.
*/
fun areBlank() = subject.value.isBlank() && body.value.isBlank()
fun areBlank() = subject.value.isBlank() &&
body.value.isBlank() &&
recipientsTo.value.isEmpty() &&
recipientsCc.value.isEmpty() &&
recipientsBcc.value.isEmpty()
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail 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.
*
* Proton Mail 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 Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.mailcomposer.domain.model
import ch.protonmail.android.mailmessage.domain.entity.Recipient
@JvmInline
value class RecipientsBcc(val value: List<Recipient>)
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail 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.
*
* Proton Mail 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 Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.mailcomposer.domain.model
import ch.protonmail.android.mailmessage.domain.entity.Recipient
@JvmInline
value class RecipientsCc(val value: List<Recipient>)
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail 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.
*
* Proton Mail 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 Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.mailcomposer.domain.model
import ch.protonmail.android.mailmessage.domain.entity.Recipient
@JvmInline
value class RecipientsTo(val value: List<Recipient>)
@@ -29,7 +29,8 @@ import javax.inject.Inject
class StoreDraftWithAllFields @Inject constructor(
private val storeDraftWithSubject: StoreDraftWithSubject,
private val storeDraftWithBody: StoreDraftWithBody
private val storeDraftWithBody: StoreDraftWithBody,
private val storeDraftWithRecipients: StoreDraftWithRecipients
) {
suspend operator fun invoke(
@@ -40,6 +41,14 @@ class StoreDraftWithAllFields @Inject constructor(
withContext(NonCancellable) {
storeDraftWithBody(draftMessageId, fields.body, fields.sender, userId).logError(draftMessageId)
storeDraftWithSubject(userId, draftMessageId, fields.sender, fields.subject).logError(draftMessageId)
storeDraftWithRecipients(
userId,
draftMessageId,
fields.sender,
fields.recipientsTo.value,
fields.recipientsCc.value,
fields.recipientsBcc.value
).logError(draftMessageId)
}
}
@@ -24,10 +24,14 @@ import ch.protonmail.android.mailcommon.domain.sample.UserAddressSample
import ch.protonmail.android.mailcommon.domain.sample.UserIdSample
import ch.protonmail.android.mailcomposer.domain.model.DraftBody
import ch.protonmail.android.mailcomposer.domain.model.DraftFields
import ch.protonmail.android.mailcomposer.domain.model.RecipientsBcc
import ch.protonmail.android.mailcomposer.domain.model.RecipientsCc
import ch.protonmail.android.mailcomposer.domain.model.RecipientsTo
import ch.protonmail.android.mailcomposer.domain.model.SenderEmail
import ch.protonmail.android.mailcomposer.domain.model.Subject
import ch.protonmail.android.mailmessage.domain.entity.MessageId
import ch.protonmail.android.mailmessage.domain.sample.MessageIdSample
import ch.protonmail.android.mailmessage.domain.sample.RecipientSample
import ch.protonmail.android.test.utils.rule.LoggingTestRule
import io.mockk.coEvery
import io.mockk.coVerify
@@ -44,10 +48,12 @@ class StoreDraftWithAllFieldsTest {
private val storeDraftWithSubjectMock = mockk<StoreDraftWithSubject>()
private val storeDraftWithBodyMock = mockk<StoreDraftWithBody>()
private val storeDraftWithRecipientsMock = mockk<StoreDraftWithRecipients>()
private val storeDraftWithAllFields = StoreDraftWithAllFields(
storeDraftWithSubjectMock,
storeDraftWithBodyMock
storeDraftWithBodyMock,
storeDraftWithRecipientsMock
)
@Test
@@ -58,9 +64,25 @@ class StoreDraftWithAllFieldsTest {
val senderEmail = SenderEmail(UserAddressSample.PrimaryAddress.email)
val subject = Subject("Subject of this email")
val plaintextDraftBody = DraftBody("I am plaintext")
val draftFields = DraftFields(senderEmail, subject, plaintextDraftBody)
val recipientsTo = RecipientsTo(listOf(RecipientSample.John))
val recipientsCc = RecipientsCc(listOf(RecipientSample.John))
val recipientsBcc = RecipientsBcc(listOf(RecipientSample.John))
val draftFields = DraftFields(
senderEmail,
subject,
plaintextDraftBody,
recipientsTo,
recipientsCc,
recipientsBcc
)
expectStoreDraftBodySucceeds(draftMessageId, plaintextDraftBody, senderEmail, userId)
expectStoreDraftSubjectSucceeds(userId, draftMessageId, senderEmail, subject)
expectStoreDraftRecipientsSucceeds(
userId,
draftMessageId,
senderEmail,
Triple(recipientsTo, recipientsCc, recipientsBcc)
)
// When
storeDraftWithAllFields(userId, draftMessageId, draftFields)
@@ -68,6 +90,16 @@ class StoreDraftWithAllFieldsTest {
// Then
coVerify { storeDraftWithBodyMock(draftMessageId, plaintextDraftBody, senderEmail, userId) }
coVerify { storeDraftWithSubjectMock(userId, draftMessageId, senderEmail, subject) }
coVerify {
storeDraftWithRecipientsMock(
userId,
draftMessageId,
senderEmail,
recipientsTo.value,
recipientsCc.value,
recipientsBcc.value
)
}
}
@Test
@@ -78,10 +110,26 @@ class StoreDraftWithAllFieldsTest {
val senderEmail = SenderEmail(UserAddressSample.PrimaryAddress.email)
val subject = Subject("Subject of this email")
val plaintextDraftBody = DraftBody("I am plaintext")
val draftFields = DraftFields(senderEmail, subject, plaintextDraftBody)
val recipientsTo = RecipientsTo(listOf(RecipientSample.John))
val recipientsCc = RecipientsCc(listOf(RecipientSample.John))
val recipientsBcc = RecipientsBcc(listOf(RecipientSample.John))
val draftFields = DraftFields(
senderEmail,
subject,
plaintextDraftBody,
recipientsTo,
recipientsCc,
recipientsBcc
)
val expectedError = StoreDraftWithBodyError.DraftReadError
expectStoreDraftBodyFails(draftMessageId, plaintextDraftBody, senderEmail, userId) { expectedError }
expectStoreDraftSubjectSucceeds(userId, draftMessageId, senderEmail, subject)
expectStoreDraftRecipientsSucceeds(
userId,
draftMessageId,
senderEmail,
Triple(recipientsTo, recipientsCc, recipientsBcc)
)
// When
storeDraftWithAllFields(userId, draftMessageId, draftFields)
@@ -99,10 +147,63 @@ class StoreDraftWithAllFieldsTest {
val senderEmail = SenderEmail(UserAddressSample.PrimaryAddress.email)
val subject = Subject("Subject of this email")
val plaintextDraftBody = DraftBody("I am plaintext")
val draftFields = DraftFields(senderEmail, subject, plaintextDraftBody)
val recipientsTo = RecipientsTo(listOf(RecipientSample.John))
val recipientsCc = RecipientsCc(listOf(RecipientSample.John))
val recipientsBcc = RecipientsBcc(listOf(RecipientSample.John))
val draftFields = DraftFields(
senderEmail,
subject,
plaintextDraftBody,
recipientsTo,
recipientsCc,
recipientsBcc
)
val expectedError = StoreDraftWithSubject.Error.DraftSaveError
expectStoreDraftBodySucceeds(draftMessageId, plaintextDraftBody, senderEmail, userId)
expectStoreDraftSubjectFails(draftMessageId, senderEmail, userId, subject) { expectedError }
expectStoreDraftRecipientsSucceeds(
userId,
draftMessageId,
senderEmail,
Triple(recipientsTo, recipientsCc, recipientsBcc)
)
// When
storeDraftWithAllFields(userId, draftMessageId, draftFields)
// Then
val expectedLog = "Storing all draft fields failed due to $expectedError. \n Draft MessageId = $draftMessageId"
loggingTestRule.assertErrorLogged(expectedLog)
}
@Test
fun `logs error when store draft with recipients fails`() = runTest {
// Given
val userId = UserIdSample.Primary
val draftMessageId = MessageIdSample.build()
val senderEmail = SenderEmail(UserAddressSample.PrimaryAddress.email)
val subject = Subject("Subject of this email")
val plaintextDraftBody = DraftBody("I am plaintext")
val recipientsTo = RecipientsTo(listOf(RecipientSample.John))
val recipientsCc = RecipientsCc(listOf(RecipientSample.John))
val recipientsBcc = RecipientsBcc(listOf(RecipientSample.John))
val draftFields = DraftFields(
senderEmail,
subject,
plaintextDraftBody,
recipientsTo,
recipientsCc,
recipientsBcc
)
val expectedError = StoreDraftWithRecipients.Error.DraftSaveError
expectStoreDraftBodySucceeds(draftMessageId, plaintextDraftBody, senderEmail, userId)
expectStoreDraftSubjectSucceeds(userId, draftMessageId, senderEmail, subject)
expectStoreDraftRecipientsFails(
draftMessageId,
senderEmail,
userId,
Triple(recipientsTo, recipientsCc, recipientsBcc)
) { expectedError }
// When
storeDraftWithAllFields(userId, draftMessageId, draftFields)
@@ -178,4 +279,41 @@ class StoreDraftWithAllFieldsTest {
} returns it.left()
}
private fun expectStoreDraftRecipientsSucceeds(
expectedUserId: UserId,
expectedMessageId: MessageId,
expectedSenderEmail: SenderEmail,
recipients: Triple<RecipientsTo, RecipientsCc, RecipientsBcc>
) {
coEvery {
storeDraftWithRecipientsMock(
expectedUserId,
expectedMessageId,
expectedSenderEmail,
recipients.first.value,
recipients.second.value,
recipients.third.value
)
} returns Unit.right()
}
private fun expectStoreDraftRecipientsFails(
expectedMessageId: MessageId,
expectedSenderEmail: SenderEmail,
expectedUserId: UserId,
recipients: Triple<RecipientsTo, RecipientsCc, RecipientsBcc>,
error: () -> StoreDraftWithRecipients.Error
) = error().also {
coEvery {
storeDraftWithRecipientsMock(
expectedUserId,
expectedMessageId,
expectedSenderEmail,
recipients.first.value,
recipients.second.value,
recipients.third.value
)
} returns it.left()
}
}
@@ -24,6 +24,9 @@ import arrow.core.getOrElse
import ch.protonmail.android.mailcommon.domain.usecase.ObservePrimaryUserId
import ch.protonmail.android.mailcomposer.domain.model.DraftBody
import ch.protonmail.android.mailcomposer.domain.model.DraftFields
import ch.protonmail.android.mailcomposer.domain.model.RecipientsBcc
import ch.protonmail.android.mailcomposer.domain.model.RecipientsCc
import ch.protonmail.android.mailcomposer.domain.model.RecipientsTo
import ch.protonmail.android.mailcomposer.domain.model.SenderEmail
import ch.protonmail.android.mailcomposer.domain.model.Subject
import ch.protonmail.android.mailcomposer.domain.usecase.GetComposerSenderAddresses
@@ -110,7 +113,14 @@ class ComposerViewModel @Inject constructor(
fun validateEmailAddress(emailAddress: String): Boolean = isValidEmailAddress(emailAddress)
private suspend fun onCloseComposer(action: ComposerAction.OnCloseComposer): ComposerOperation {
val fields = DraftFields(currentSenderEmail(), currentSubject(), currentDraftBody())
val fields = DraftFields(
currentSenderEmail(),
currentSubject(),
currentDraftBody(),
currentValidRecipientsTo(),
currentValidRecipientsCc(),
currentValidRecipientsBcc()
)
return when {
fields.areBlank() -> action
else -> {
@@ -152,6 +162,24 @@ class ComposerViewModel @Inject constructor(
private fun currentSenderEmail() = SenderEmail(state.value.fields.sender.email)
private suspend fun currentValidRecipientsTo() = RecipientsTo(
state.value.fields.to.filterIsInstance<RecipientUiModel.Valid>().map {
participantMapper.recipientUiModelToParticipant(it, contactsOrEmpty())
}
)
private suspend fun currentValidRecipientsCc() = RecipientsCc(
state.value.fields.cc.filterIsInstance<RecipientUiModel.Valid>().map {
participantMapper.recipientUiModelToParticipant(it, contactsOrEmpty())
}
)
private suspend fun currentValidRecipientsBcc() = RecipientsBcc(
state.value.fields.bcc.filterIsInstance<RecipientUiModel.Valid>().map {
participantMapper.recipientUiModelToParticipant(it, contactsOrEmpty())
}
)
private suspend fun contactsOrEmpty() = getContacts(primaryUserId()).getOrElse { emptyList() }
private suspend fun onChangeSender() = getComposerSenderAddresses().fold(
@@ -28,6 +28,9 @@ import ch.protonmail.android.mailcommon.presentation.Effect
import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
import ch.protonmail.android.mailcomposer.domain.model.DraftBody
import ch.protonmail.android.mailcomposer.domain.model.DraftFields
import ch.protonmail.android.mailcomposer.domain.model.RecipientsBcc
import ch.protonmail.android.mailcomposer.domain.model.RecipientsCc
import ch.protonmail.android.mailcomposer.domain.model.RecipientsTo
import ch.protonmail.android.mailcomposer.domain.model.SenderEmail
import ch.protonmail.android.mailcomposer.domain.model.Subject
import ch.protonmail.android.mailcomposer.domain.usecase.GetComposerSenderAddresses
@@ -51,6 +54,7 @@ import ch.protonmail.android.mailcontact.domain.usecase.GetContacts
import ch.protonmail.android.mailmessage.domain.entity.MessageId
import ch.protonmail.android.mailmessage.domain.entity.Recipient
import ch.protonmail.android.mailmessage.domain.sample.MessageIdSample
import ch.protonmail.android.mailmessage.domain.sample.RecipientSample
import ch.protonmail.android.test.utils.rule.LoggingTestRule
import ch.protonmail.android.test.utils.rule.MainDispatcherRule
import ch.protonmail.android.testdata.contact.ContactSample
@@ -63,6 +67,7 @@ import io.mockk.mockkObject
import io.mockk.unmockkObject
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import me.proton.core.contact.domain.entity.Contact
import me.proton.core.domain.entity.UserId
import me.proton.core.user.domain.entity.UserAddress
import kotlin.test.AfterTest
@@ -314,11 +319,28 @@ class ComposerViewModelTest {
val expectedUserId = expectedUserId { UserIdSample.Primary }
val expectedDraftBody = DraftBody("I am plaintext")
expectedPrimaryAddress(expectedUserId) { UserAddressSample.PrimaryAddress }
val expectedFields = DraftFields(expectedSenderEmail, expectedSubject, expectedDraftBody)
val recipientsTo = RecipientsTo(listOf(RecipientSample.John))
val recipientsCc = RecipientsCc(listOf(RecipientSample.John))
val recipientsBcc = RecipientsBcc(listOf(RecipientSample.John))
val expectedFields = DraftFields(
expectedSenderEmail,
expectedSubject,
expectedDraftBody,
recipientsTo,
recipientsCc,
recipientsBcc
)
mockParticipantMapper()
expectStoreAllDraftFieldsSucceeds(expectedUserId, expectedMessageId, expectedFields)
// Change internal state of the View Model to simulate the existence of all fields before closing the composer
expectedViewModelInitialState(expectedMessageId, expectedSenderEmail, expectedSubject, expectedDraftBody)
expectedViewModelInitialState(
expectedMessageId,
expectedSenderEmail,
expectedSubject,
expectedDraftBody,
Triple(recipientsTo, recipientsCc, recipientsBcc)
)
// When
viewModel.submit(ComposerAction.OnCloseComposer)
@@ -352,11 +374,27 @@ class ComposerViewModelTest {
val expectedMessageId = expectedMessageId { MessageIdSample.EmptyDraft }
val expectedUserId = expectedUserId { UserIdSample.Primary }
expectedPrimaryAddress(expectedUserId) { UserAddressSample.PrimaryAddress }
val expectedFields = DraftFields(expectedSenderEmail, expectedSubject, expectedDraftBody)
val recipientsTo = RecipientsTo(listOf(RecipientSample.John))
val recipientsCc = RecipientsCc(listOf(RecipientSample.John))
val recipientsBcc = RecipientsBcc(listOf(RecipientSample.John))
val expectedFields = DraftFields(
expectedSenderEmail,
expectedSubject,
expectedDraftBody,
recipientsTo,
recipientsCc,
recipientsBcc
)
mockParticipantMapper()
expectStoreAllDraftFieldsSucceeds(expectedUserId, expectedMessageId, expectedFields)
// Change internal state of the View Model to simulate the existence of all fields before closing the composer
expectedViewModelInitialState(expectedMessageId, expectedSenderEmail, expectedSubject)
expectedViewModelInitialState(
expectedMessageId,
expectedSenderEmail,
expectedSubject,
recipients = Triple(recipientsTo, recipientsCc, recipientsBcc)
)
// When
viewModel.submit(ComposerAction.OnCloseComposer)
@@ -635,15 +673,20 @@ class ComposerViewModelTest {
messageId: MessageId,
senderEmail: SenderEmail = SenderEmail(""),
subject: Subject = Subject(""),
draftBody: DraftBody = DraftBody("")
draftBody: DraftBody = DraftBody(""),
recipients: Triple<RecipientsTo, RecipientsCc, RecipientsBcc> = Triple(
RecipientsTo(emptyList()),
RecipientsCc(emptyList()),
RecipientsBcc(emptyList())
)
) {
val expected = ComposerDraftState(
fields = ComposerFields(
messageId,
SenderUiModel(senderEmail.value),
emptyList(),
emptyList(),
emptyList(),
recipients.first.value.map { RecipientUiModel.Valid(it.address) },
recipients.second.value.map { RecipientUiModel.Valid(it.address) },
recipients.third.value.map { RecipientUiModel.Valid(it.address) },
subject.value,
draftBody.value
),
@@ -804,15 +847,26 @@ class ComposerViewModelTest {
} returns Unit
}
private fun mockParticipantMapper() {
private fun expectContacts(): List<Contact> {
val expectedContacts = listOf(ContactSample.Doe, ContactSample.John)
coEvery { getContactsMock.invoke(UserIdSample.Primary) } returns expectedContacts.right()
return expectedContacts
}
private fun mockParticipantMapper() {
val expectedContacts = expectContacts()
every {
participantMapperMock.recipientUiModelToParticipant(
RecipientUiModel.Valid("valid@email.com"),
expectedContacts
)
} returns Recipient("valid@email.com", "Valid Email", false)
every {
participantMapperMock.recipientUiModelToParticipant(
RecipientUiModel.Valid(RecipientSample.John.address),
any()
)
} returns Recipient(RecipientSample.John.address, RecipientSample.John.name, false)
}
companion object TestData {