Add Block Sender confirmation dialog

- If the sender is a contact, then the title changes to Block this contact
 - Implemented dialog state and reducer
 - Added view actions for the dialog

ET-5145
This commit is contained in:
Serdar Ozturk
2025-11-03 14:36:58 +00:00
committed by MargeBot
parent f5124c7959
commit 398cd9af2b
19 changed files with 449 additions and 45 deletions
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 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.maildetail.presentation.model
import ch.protonmail.android.mailcontact.domain.model.ContactId
sealed interface BlockSenderDialogState {
object Hidden : BlockSenderDialogState
sealed interface Shown : BlockSenderDialogState {
data class ShowConfirmation(
val messageId: MessageIdUiModel?,
val email: String,
val contactId: ContactId?
) : Shown
}
}
@@ -27,6 +27,7 @@ import ch.protonmail.android.mailattachments.domain.model.OpenAttachmentIntentVa
import ch.protonmail.android.mailcommon.presentation.model.AvatarUiModel
import ch.protonmail.android.mailcommon.presentation.model.BottomBarEvent
import ch.protonmail.android.mailcommon.presentation.model.BottomSheetOperation
import ch.protonmail.android.mailcontact.domain.model.ContactId
import ch.protonmail.android.mailconversation.domain.entity.HiddenMessagesBanner
import ch.protonmail.android.maildetail.domain.model.OpenProtonCalendarIntentValues
import ch.protonmail.android.maildetail.presentation.R
@@ -39,6 +40,7 @@ import ch.protonmail.android.maildetail.presentation.model.ConversationDetailOpe
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailOperation.AffectingMessageBar
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailOperation.AffectingMessages
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailOperation.AffectingReportPhishingDialog
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailOperation.AffectingBlockSenderDialog
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailOperation.AffectingHiddenMessagesBanner
import ch.protonmail.android.maillabel.domain.model.LabelId
import ch.protonmail.android.maillabel.domain.model.SystemLabelId
@@ -63,6 +65,8 @@ sealed interface ConversationDetailOperation {
sealed interface AffectingHiddenMessagesBanner
sealed interface AffectingMarkAsLegitimateDialog
sealed interface AffectingEditScheduleMessageDialog
sealed interface AffectingBlockSenderDialog
}
sealed interface ConversationDetailEvent : ConversationDetailOperation {
@@ -350,8 +354,13 @@ sealed interface ConversationDetailViewAction : ConversationDetailOperation {
data class UnblockSender(val messageId: MessageIdUiModel?, val email: String) :
ConversationDetailViewAction, AffectingBottomSheet
data class BlockSender(val messageId: MessageIdUiModel?, val email: String) :
ConversationDetailViewAction, AffectingBottomSheet
data class BlockSender(val messageId: MessageIdUiModel?, val email: String, val contactId: ContactId?) :
ConversationDetailViewAction, AffectingBottomSheet, AffectingBlockSenderDialog
object BlockSenderDismissed : ConversationDetailViewAction, AffectingBlockSenderDialog
data class BlockSenderConfirmed(
val messageId: MessageIdUiModel?,
val email: String
) : ConversationDetailViewAction, AffectingBlockSenderDialog
data object EditScheduleSendMessageDismissed :
ConversationDetailViewAction, AffectingEditScheduleMessageDialog
@@ -46,7 +46,8 @@ data class ConversationDetailState(
val reportPhishingDialogState: ReportPhishingDialogState,
val hiddenMessagesBannerState: HiddenMessagesBannerState,
val markAsLegitimateDialogState: MarkAsLegitimateDialogState,
val editScheduledMessageDialogState: EditScheduledMessageDialogState
val editScheduledMessageDialogState: EditScheduledMessageDialogState,
val blockSenderDialogState: BlockSenderDialogState
) {
companion object {
@@ -71,7 +72,8 @@ data class ConversationDetailState(
reportPhishingDialogState = ReportPhishingDialogState.Hidden,
hiddenMessagesBannerState = HiddenMessagesBannerState.Hidden,
markAsLegitimateDialogState = MarkAsLegitimateDialogState.Hidden,
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden,
blockSenderDialogState = BlockSenderDialogState.Hidden
)
}
}
@@ -30,6 +30,7 @@ import ch.protonmail.android.mailcommon.presentation.model.contentDescription
import ch.protonmail.android.mailcommon.presentation.model.description
import ch.protonmail.android.mailcommon.presentation.model.iconDrawable
import ch.protonmail.android.mailcommon.presentation.sample.TextMessageSample
import ch.protonmail.android.maildetail.presentation.model.BlockSenderDialogState
import ch.protonmail.android.maildetail.presentation.model.ConversationDeleteState
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailMetadataState
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailState
@@ -97,7 +98,8 @@ object ConversationDetailsPreviewData {
reportPhishingDialogState = ReportPhishingDialogState.Hidden,
hiddenMessagesBannerState = HiddenMessagesBannerState.Hidden,
markAsLegitimateDialogState = MarkAsLegitimateDialogState.Hidden,
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden,
blockSenderDialogState = BlockSenderDialogState.Hidden
)
val SuccessWithRandomMessageIds = ConversationDetailState(
@@ -154,7 +156,8 @@ object ConversationDetailsPreviewData {
reportPhishingDialogState = ReportPhishingDialogState.Hidden,
hiddenMessagesBannerState = HiddenMessagesBannerState.Hidden,
markAsLegitimateDialogState = MarkAsLegitimateDialogState.Hidden,
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden,
blockSenderDialogState = BlockSenderDialogState.Hidden
)
val FailedLoadingConversation = ConversationDetailState(
@@ -177,7 +180,8 @@ object ConversationDetailsPreviewData {
reportPhishingDialogState = ReportPhishingDialogState.Hidden,
hiddenMessagesBannerState = HiddenMessagesBannerState.Hidden,
markAsLegitimateDialogState = MarkAsLegitimateDialogState.Hidden,
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden,
blockSenderDialogState = BlockSenderDialogState.Hidden
)
val FailedLoadingMessages = ConversationDetailState(
@@ -200,7 +204,8 @@ object ConversationDetailsPreviewData {
reportPhishingDialogState = ReportPhishingDialogState.Hidden,
hiddenMessagesBannerState = HiddenMessagesBannerState.Hidden,
markAsLegitimateDialogState = MarkAsLegitimateDialogState.Hidden,
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden,
blockSenderDialogState = BlockSenderDialogState.Hidden
)
val FailedLoadingBottomBar = ConversationDetailState(
@@ -223,7 +228,8 @@ object ConversationDetailsPreviewData {
reportPhishingDialogState = ReportPhishingDialogState.Hidden,
hiddenMessagesBannerState = HiddenMessagesBannerState.Hidden,
markAsLegitimateDialogState = MarkAsLegitimateDialogState.Hidden,
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden,
blockSenderDialogState = BlockSenderDialogState.Hidden
)
val Loading = ConversationDetailState(
@@ -246,7 +252,8 @@ object ConversationDetailsPreviewData {
reportPhishingDialogState = ReportPhishingDialogState.Hidden,
hiddenMessagesBannerState = HiddenMessagesBannerState.Hidden,
markAsLegitimateDialogState = MarkAsLegitimateDialogState.Hidden,
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden,
blockSenderDialogState = BlockSenderDialogState.Hidden
)
val NotLoggedIn = ConversationDetailState(
@@ -269,7 +276,8 @@ object ConversationDetailsPreviewData {
reportPhishingDialogState = ReportPhishingDialogState.Hidden,
hiddenMessagesBannerState = HiddenMessagesBannerState.Hidden,
markAsLegitimateDialogState = MarkAsLegitimateDialogState.Hidden,
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden
editScheduledMessageDialogState = EditScheduledMessageDialogState.Hidden,
blockSenderDialogState = BlockSenderDialogState.Hidden
)
}
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2025 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.maildetail.presentation.reducer
import ch.protonmail.android.maildetail.presentation.model.BlockSenderDialogState
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailOperation
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailViewAction
import javax.inject.Inject
class ConversationBlockSenderDialogReducer @Inject constructor() {
internal fun newStateFrom(operation: ConversationDetailOperation.AffectingBlockSenderDialog) = when (operation) {
is ConversationDetailViewAction.BlockSenderDismissed,
is ConversationDetailViewAction.BlockSenderConfirmed -> BlockSenderDialogState.Hidden
is ConversationDetailViewAction.BlockSender -> BlockSenderDialogState.Shown.ShowConfirmation(
messageId = operation.messageId,
email = operation.email,
contactId = operation.contactId
)
}
}
@@ -27,6 +27,7 @@ import ch.protonmail.android.mailcommon.presentation.reducer.BottomBarReducer
import ch.protonmail.android.maildetail.domain.model.OpenProtonCalendarIntentValues
import ch.protonmail.android.maildetail.presentation.R
import ch.protonmail.android.maildetail.presentation.mapper.ActionResultMapper
import ch.protonmail.android.maildetail.presentation.model.BlockSenderDialogState
import ch.protonmail.android.maildetail.presentation.model.ConversationDeleteState
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ConversationBottomBarEvent
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ConversationBottomSheetEvent
@@ -85,6 +86,7 @@ class ConversationDetailReducer @Inject constructor(
private val bottomSheetReducer: BottomSheetReducer,
private val deleteDialogReducer: ConversationDeleteDialogReducer,
private val reportPhishingDialogReducer: ConversationReportPhishingDialogReducer,
private val blockSenderDialogReducer: ConversationBlockSenderDialogReducer,
private val hiddenMessagesBannerReducer: HiddenMessagesBannerReducer,
private val markAsLegitimateDialogReducer: MarkAsLegitimateDialogReducer,
private val editScheduledMessageDialogReducer: EditScheduledMessageDialogReducer,
@@ -114,7 +116,8 @@ class ConversationDetailReducer @Inject constructor(
reportPhishingDialogState = currentState.toNewReportPhishingDialogState(operation),
hiddenMessagesBannerState = currentState.toNewHiddenMessagesBannerState(operation),
markAsLegitimateDialogState = currentState.toNewMarkAsLegitimateDialogState(operation),
editScheduledMessageDialogState = currentState.toNewEditScheduleMessageDialogState(operation)
editScheduledMessageDialogState = currentState.toNewEditScheduleMessageDialogState(operation),
blockSenderDialogState = currentState.toNewBlockSenderDialogState(operation)
)
}
@@ -380,6 +383,16 @@ class ConversationDetailReducer @Inject constructor(
}
}
private fun ConversationDetailState.toNewBlockSenderDialogState(
operation: ConversationDetailOperation
): BlockSenderDialogState {
return if (operation is ConversationDetailOperation.AffectingBlockSenderDialog) {
blockSenderDialogReducer.newStateFrom(operation)
} else {
blockSenderDialogState
}
}
private fun ConversationDetailState.toNewHiddenMessagesBannerState(
operation: ConversationDetailOperation
): HiddenMessagesBannerState {
@@ -116,6 +116,7 @@ import ch.protonmail.android.maildetail.presentation.model.MessageIdUiModel
import ch.protonmail.android.maildetail.presentation.model.MoreActionsBottomSheetEntryPoint
import ch.protonmail.android.maildetail.presentation.model.ParticipantUiModel
import ch.protonmail.android.maildetail.presentation.previewdata.ConversationDetailsPreviewProvider
import ch.protonmail.android.maildetail.presentation.ui.dialog.BlockSenderDialog
import ch.protonmail.android.maildetail.presentation.ui.dialog.EditScheduleSendDialog
import ch.protonmail.android.maildetail.presentation.ui.dialog.MarkAsLegitimateDialog
import ch.protonmail.android.maildetail.presentation.ui.dialog.ReportPhishingDialog
@@ -242,6 +243,13 @@ fun ConversationDetailScreen(
onDismiss = { viewModel.submit(ConversationDetailViewAction.EditScheduleSendMessageDismissed) }
)
BlockSenderDialog(
state = state.blockSenderDialogState,
onConfirm = { messageId, email ->
viewModel.submit(ConversationDetailViewAction.BlockSenderConfirmed(messageId, email))
},
onDismiss = { viewModel.submit(ConversationDetailViewAction.BlockSenderDismissed) }
)
ProtonModalBottomSheetLayout(
showBottomSheet = showBottomSheet,
@@ -435,10 +443,12 @@ fun ConversationDetailScreen(
viewModel.submit(ConversationDetailViewAction.DismissBottomSheet)
actions.onComposeNewMessage(it.address)
},
onBlockClicked = { participant, messageId ->
onBlockClicked = { participant, messageId, contactId ->
viewModel.submit(
ConversationDetailViewAction.BlockSender(
messageId?.let { MessageIdUiModel(it.id) }, participant.address
messageId = messageId?.let { MessageIdUiModel(it.id) },
email = participant.address,
contactId = contactId
)
)
@@ -0,0 +1,81 @@
/*
* 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.maildetail.presentation.ui.dialog
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import ch.protonmail.android.maildetail.presentation.R
import ch.protonmail.android.design.compose.component.ProtonAlertDialog
import ch.protonmail.android.design.compose.component.ProtonAlertDialogButton
import ch.protonmail.android.design.compose.component.ProtonAlertDialogText
import ch.protonmail.android.design.compose.theme.ProtonTheme
import ch.protonmail.android.mailcontact.domain.model.ContactId
import ch.protonmail.android.maildetail.presentation.model.BlockSenderDialogState
import ch.protonmail.android.maildetail.presentation.model.MessageIdUiModel
@Composable
fun BlockSenderDialog(
state: BlockSenderDialogState,
onDismiss: () -> Unit,
onConfirm: (MessageIdUiModel?, String) -> Unit
) {
if (state is BlockSenderDialogState.Shown) {
when (state) {
is BlockSenderDialogState.Shown.ShowConfirmation -> BlockSenderDialog(
email = state.email,
contactId = state.contactId,
onDismiss = onDismiss,
onConfirm = { onConfirm(state.messageId, state.email) }
)
}
}
}
@Composable
private fun BlockSenderDialog(
email: String,
contactId: ContactId?,
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
ProtonAlertDialog(
onDismissRequest = onDismiss,
confirmButton = {
ProtonAlertDialogButton(
titleResId = R.string.contact_actions_block_contact_dialog_button_block,
textColor = ProtonTheme.colors.notificationError
) { onConfirm() }
},
dismissButton = {
ProtonAlertDialogButton(R.string.contact_actions_block_contact_dialog_button_cancel) { onDismiss() }
},
title = stringResource(
id = contactId?.let { R.string.contact_actions_block_contact }
?: R.string.contact_actions_block_address
),
text = {
ProtonAlertDialogText(
text = stringResource(
id = R.string.contact_actions_block_contact_dialog_text,
email
)
)
}
)
}
@@ -387,6 +387,8 @@ class ConversationDetailViewModel @Inject constructor(
is ScrollRequestCompleted,
is ConversationDetailViewAction.ReportPhishing,
is ConversationDetailViewAction.ReportPhishingDismissed,
is ConversationDetailViewAction.BlockSender,
is ConversationDetailViewAction.BlockSenderDismissed,
is ConversationDetailViewAction.MarkMessageAsLegitimate,
is ConversationDetailViewAction.MarkMessageAsLegitimateDismissed,
is ConversationDetailViewAction.EditScheduleSendMessageDismissed,
@@ -429,7 +431,7 @@ class ConversationDetailViewModel @Inject constructor(
handleMarkMessageAsLegitimateConfirmed(action)
is ConversationDetailViewAction.UnblockSender -> handleUnblockSender(action)
is ConversationDetailViewAction.BlockSender -> handleBlockSender(action)
is ConversationDetailViewAction.BlockSenderConfirmed -> handleBlockSenderConfirmed(action)
is ConversationDetailViewAction.EditScheduleSendMessageConfirmed -> handleEditScheduleSendMessage(action)
is ConversationDetailViewAction.PrintMessage -> handlePrintMessage(action.context, action.messageId)
is ConversationDetailViewAction.RetryRsvpEventLoading ->
@@ -1693,19 +1695,20 @@ class ConversationDetailViewModel @Inject constructor(
}
}
private fun handleBlockSender(action: ConversationDetailViewAction.BlockSender) = viewModelScope.launch {
private fun handleBlockSenderConfirmed(action: ConversationDetailViewAction.BlockSenderConfirmed) =
viewModelScope.launch {
blockSender(
userId = primaryUserId.first(),
email = action.email
).fold(
ifLeft = { Timber.e("Failed to block sender in message ${action.messageId?.id}") },
ifRight = { action.messageId?.let { setOrRefreshMessageBody(it) } }
)
viewModelScope.launch {
blockSender(
userId = primaryUserId.first(),
email = action.email
).fold(
ifLeft = { Timber.e("Failed to block sender in message ${action.messageId?.id}") },
ifRight = { action.messageId?.let { setOrRefreshMessageBody(it) } }
)
emitNewStateFrom(action)
emitNewStateFrom(action)
}
}
}
private fun handleGetRsvpEvent(messageId: MessageId, refresh: Boolean) {
viewModelScope.launch {
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2025 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.maildetail.presentation.reducer
import ch.protonmail.android.maildetail.presentation.model.BlockSenderDialogState
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailOperation
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailViewAction
import ch.protonmail.android.maildetail.presentation.model.MessageIdUiModel
import ch.protonmail.android.testdata.contact.ContactIdSample
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
class ConversationBlockSenderDialogReducerTest(
private val testName: String,
private val testInput: TestInput
) {
private val blockSenderDialogReducer = ConversationBlockSenderDialogReducer()
@Test
fun `should produce the expected new state`() = with(testInput) {
val actualState = blockSenderDialogReducer.newStateFrom(operation)
assertEquals(testName, expectedState, actualState)
}
companion object {
private val transitions = listOf(
TestInput(
operation = ConversationDetailViewAction.BlockSenderDismissed,
expectedState = BlockSenderDialogState.Hidden
),
TestInput(
operation = ConversationDetailViewAction.BlockSenderConfirmed(
MessageIdUiModel("msg-123"), "test@example.com"
),
expectedState = BlockSenderDialogState.Hidden
),
TestInput(
operation = ConversationDetailViewAction.BlockSender(
messageId = MessageIdUiModel("HtmlInvoice"),
email = "test@example.com",
contactId = ContactIdSample.Doe
),
expectedState = BlockSenderDialogState.Shown.ShowConfirmation(
messageId = MessageIdUiModel("HtmlInvoice"),
email = "test@example.com",
contactId = ContactIdSample.Doe
)
)
)
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun data(): Collection<Array<Any>> {
return transitions.map {
val testName = """
Operation: ${it.operation}
Expected State: ${it.expectedState}
""".trimIndent()
arrayOf(testName, it)
}
}
data class TestInput(
val operation: ConversationDetailOperation.AffectingBlockSenderDialog,
val expectedState: BlockSenderDialogState
)
}
}
@@ -78,6 +78,7 @@ class ConversationDetailReducerTest(
private val bottomSheetReducer = mockk<BottomSheetReducer>(relaxed = true)
private val deleteDialogReducer = mockk<ConversationDeleteDialogReducer>(relaxed = true)
private val reportPhishingDialogReducer = mockk<ConversationReportPhishingDialogReducer>(relaxed = true)
private val blockSenderDialogReducer = mockk<ConversationBlockSenderDialogReducer>(relaxed = true)
private val trashedMessagesBannerReducer = mockk<HiddenMessagesBannerReducer>(relaxed = true)
private val markAsLegitimateDialogReducer = mockk<MarkAsLegitimateDialogReducer>(relaxed = true)
private val editScheduledMessageDialogReducer = mockk<EditScheduledMessageDialogReducer>(relaxed = true)
@@ -88,6 +89,7 @@ class ConversationDetailReducerTest(
bottomSheetReducer = bottomSheetReducer,
deleteDialogReducer = deleteDialogReducer,
reportPhishingDialogReducer = reportPhishingDialogReducer,
blockSenderDialogReducer = blockSenderDialogReducer,
hiddenMessagesBannerReducer = trashedMessagesBannerReducer,
markAsLegitimateDialogReducer = markAsLegitimateDialogReducer,
editScheduledMessageDialogReducer = editScheduledMessageDialogReducer,
@@ -181,6 +183,12 @@ class ConversationDetailReducerTest(
verify { reportPhishingDialogReducer wasNot Called }
}
if (reducesBlockSenderDialog) {
verify { blockSenderDialogReducer.newStateFrom(any()) }
} else {
verify { blockSenderDialogReducer wasNot Called }
}
if (reducesMarkAsLegitimateDialog) {
verify { markAsLegitimateDialogReducer.newStateFrom(any()) }
} else {
@@ -210,6 +218,7 @@ class ConversationDetailReducerTest(
val reducesDeleteDialog: Boolean,
val reducesTrashedMessagesBanner: Boolean,
val reducesReportPhishingDialog: Boolean,
val reducesBlockSenderDialog: Boolean,
val reducesMarkAsLegitimateDialog: Boolean,
val reducesEditScheduleSendDialog: Boolean
) {
@@ -278,6 +287,13 @@ class ConversationDetailReducerTest(
MessageId(messageId.id)
) affects listOf(ReportPhishingDialog),
ConversationDetailViewAction.ReportPhishingDismissed affects listOf(ReportPhishingDialog),
ConversationDetailViewAction.BlockSender(
MessageIdUiModel(messageId.id), participant.participantAddress, null
) affects listOf(BottomSheet, BlockSenderDialog),
ConversationDetailViewAction.BlockSenderConfirmed(
MessageIdUiModel(messageId.id), participant.participantAddress
) affects listOf(BlockSenderDialog),
ConversationDetailViewAction.BlockSenderDismissed affects listOf(BlockSenderDialog),
ConversationDetailViewAction.MarkMessageAsLegitimate(
MessageId(messageId.id), isPhishing = true
) affects listOf(MarkAsLegitimateDialog),
@@ -401,6 +417,7 @@ private infix fun ConversationDetailOperation.affects(entities: List<Entity>) =
reducesDeleteDialog = entities.contains(DeleteDialog),
reducesTrashedMessagesBanner = entities.contains(HiddenMessagesBanner),
reducesReportPhishingDialog = entities.contains(ReportPhishingDialog),
reducesBlockSenderDialog = entities.contains(BlockSenderDialog),
reducesMarkAsLegitimateDialog = entities.contains(MarkAsLegitimateDialog),
reducesEditScheduleSendDialog = entities.contains(EditScheduleSendDialog)
)
@@ -422,6 +439,7 @@ private data object MessageScroll : Entity
private data object DeleteDialog : Entity
private data object HiddenMessagesBanner : Entity
private data object ReportPhishingDialog : Entity
private data object BlockSenderDialog : Entity
private data object MarkAsLegitimateDialog : Entity
private data object EditScheduleSendDialog : Entity
@@ -126,6 +126,7 @@ import ch.protonmail.android.maildetail.presentation.model.MarkAsLegitimateDialo
import ch.protonmail.android.maildetail.presentation.model.MessageIdUiModel
import ch.protonmail.android.maildetail.presentation.model.MoreActionsBottomSheetEntryPoint
import ch.protonmail.android.maildetail.presentation.model.ReportPhishingDialogState
import ch.protonmail.android.maildetail.presentation.reducer.ConversationBlockSenderDialogReducer
import ch.protonmail.android.maildetail.presentation.reducer.ConversationDeleteDialogReducer
import ch.protonmail.android.maildetail.presentation.reducer.ConversationDetailMessagesReducer
import ch.protonmail.android.maildetail.presentation.reducer.ConversationDetailMetadataReducer
@@ -483,6 +484,7 @@ internal class ConversationDetailViewModelIntegrationTest {
),
deleteDialogReducer = ConversationDeleteDialogReducer(),
reportPhishingDialogReducer = ConversationReportPhishingDialogReducer(),
blockSenderDialogReducer = ConversationBlockSenderDialogReducer(),
hiddenMessagesBannerReducer = HiddenMessagesBannerReducer(),
markAsLegitimateDialogReducer = MarkAsLegitimateDialogReducer(),
editScheduledMessageDialogReducer = EditScheduledMessageDialogReducer(),
@@ -2043,7 +2043,7 @@ class ConversationDetailViewModelTest {
// When
viewModel.state.test {
initialStateEmitted()
viewModel.submit(ConversationDetailViewAction.BlockSender(messageIdUiModel, email))
viewModel.submit(ConversationDetailViewAction.BlockSenderConfirmed(messageIdUiModel, email))
advanceUntilIdle()
@@ -2053,6 +2053,23 @@ class ConversationDetailViewModelTest {
}
}
@Test
fun `do not call block sender when dismissed`() = runTest {
// Given
viewModel.state.test {
initialStateEmitted()
// When
viewModel.submit(ConversationDetailViewAction.BlockSenderDismissed)
advanceUntilIdle()
// Then
coVerify(exactly = 0) { blockSender(userId, any()) }
cancelAndIgnoreRemainingEvents()
}
}
@Suppress("LongMethod")
private fun setupCollapsedToExpandMessagesState(
withUnreadMessage: Boolean = false
@@ -20,6 +20,7 @@ package ch.protonmail.android.mailmessage.presentation.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import ch.protonmail.android.mailcontact.domain.model.ContactId
import ch.protonmail.android.mailmessage.domain.model.Participant
import ch.protonmail.android.mailmessage.presentation.R
@@ -53,7 +54,7 @@ sealed class ContactActionUiModel(
descriptionRes = R.string.contact_actions_add_contact_description
)
data class BlockContact(val participant: Participant) : ContactActionUiModel(
data class BlockContact(val participant: Participant, val contactId: ContactId) : ContactActionUiModel(
iconRes = R.drawable.ic_proton_circle_slash,
textRes = R.string.contact_actions_block_contact,
descriptionRes = R.string.contact_actions_block_contact_description
@@ -54,11 +54,18 @@ class ContactActionsBottomSheetReducer @Inject constructor() {
emptyList()
} else {
buildList {
val action = when {
contactId == null && isSenderBlocked -> ContactActionUiModel.UnblockAddress(participant)
contactId == null && !isSenderBlocked -> ContactActionUiModel.BlockAddress(participant)
contactId != null && isSenderBlocked -> ContactActionUiModel.UnblockContact(participant)
else -> ContactActionUiModel.BlockContact(participant)
val action = if (contactId == null) {
if (isSenderBlocked) {
ContactActionUiModel.UnblockAddress(participant)
} else {
ContactActionUiModel.BlockAddress(participant)
}
} else {
if (isSenderBlocked) {
ContactActionUiModel.UnblockContact(participant)
} else {
ContactActionUiModel.BlockContact(participant, contactId)
}
}
add(action)
}
@@ -42,6 +42,7 @@ import ch.protonmail.android.design.compose.theme.titleLargeNorm
import ch.protonmail.android.mailcommon.presentation.compose.Avatar
import ch.protonmail.android.mailcommon.presentation.compose.MailDimens
import ch.protonmail.android.mailcommon.presentation.model.AvatarUiModel
import ch.protonmail.android.mailcontact.domain.model.ContactId
import ch.protonmail.android.mailmessage.domain.model.MessageId
import ch.protonmail.android.mailmessage.domain.model.Participant
import ch.protonmail.android.mailmessage.presentation.model.ContactActionUiModel
@@ -133,9 +134,13 @@ private fun callbackForActions(
is ContactActionUiModel.CopyAddress -> actions.onCopyAddressClicked(action.address)
is ContactActionUiModel.CopyName -> actions.onCopyNameClicked(action.name)
is ContactActionUiModel.NewMessage -> actions.onNewMessageClicked(action.participant)
is ContactActionUiModel.BlockContact -> actions.onBlockClicked(action.participant, sheetOrigin.getMessageId())
is ContactActionUiModel.BlockContact -> actions.onBlockClicked(
action.participant,
sheetOrigin.getMessageId(), action.contactId
)
is ContactActionUiModel.UnblockContact -> actions.onUnblockClicked(action.participant, sheetOrigin.getMessageId())
is ContactActionUiModel.BlockAddress -> actions.onBlockClicked(action.participant, sheetOrigin.getMessageId())
is ContactActionUiModel.BlockAddress -> actions.onBlockClicked(action.participant, sheetOrigin.getMessageId(), null)
is ContactActionUiModel.UnblockAddress -> actions.onUnblockClicked(action.participant, sheetOrigin.getMessageId())
}
@@ -194,7 +199,7 @@ object ContactActionsBottomSheetContent {
val onCopyNameClicked: (name: String) -> Unit,
val onNewMessageClicked: (participant: Participant) -> Unit,
val onAddContactClicked: (participant: Participant) -> Unit,
val onBlockClicked: (participant: Participant, messageId: MessageId?) -> Unit,
val onBlockClicked: (participant: Participant, messageId: MessageId?, contactId: ContactId?) -> Unit,
val onUnblockClicked: (participant: Participant, messageId: MessageId?) -> Unit
)
}
@@ -228,7 +233,7 @@ fun ContactActionsBottomSheetContentPreview() {
onCopyNameClicked = {},
onNewMessageClicked = {},
onAddContactClicked = {},
onBlockClicked = { _, _ -> },
onBlockClicked = { _, _, _ -> },
onUnblockClicked = { _, _ -> }
)
)
@@ -16,7 +16,7 @@
~ along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="selected_count_title">
<item quantity="one">%d Selected</item>
<item quantity="other">%d Selected</item>
@@ -73,6 +73,9 @@
<string name="contact_actions_add_contact_description">Save this recipient as a contact</string>
<string name="contact_actions_block_contact">Block this contact</string>
<string name="contact_actions_block_contact_description">Block this contact</string>
<string name="contact_actions_block_contact_dialog_text">Emails from <xliff:g id="email">%1$s</xliff:g> will no longer be delivered and will be permanently deleted. You can manage blocked email addresses in the settings.</string>
<string name="contact_actions_block_contact_dialog_button_block">Block</string>
<string name="contact_actions_block_contact_dialog_button_cancel">Cancel</string>
<string name="contact_actions_unblock_contact">Block this contact</string>
<string name="contact_actions_unblock_contact_description">Block this contact</string>
<string name="contact_actions_block_address">Block this address</string>
@@ -20,11 +20,13 @@ package ch.protonmail.android.mailmessage.presentation.reducer
import ch.protonmail.android.mailcommon.presentation.model.BottomSheetState
import ch.protonmail.android.mailcommon.presentation.sample.ParticipantAvatarSample
import ch.protonmail.android.mailcontact.domain.model.ContactId
import ch.protonmail.android.mailmessage.domain.model.MessageId
import ch.protonmail.android.mailmessage.domain.model.Participant
import ch.protonmail.android.mailmessage.presentation.model.ContactActionUiModel
import ch.protonmail.android.mailmessage.presentation.model.bottomsheet.ContactActionsBottomSheetState
import ch.protonmail.android.testdata.contact.ContactActionsGroupsSample
import ch.protonmail.android.testdata.contact.ContactSample
import kotlinx.collections.immutable.toImmutableList
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -61,6 +63,7 @@ internal class ContactActionsBottomSheetReducerTest(
private val sampleOrigin =
ContactActionsBottomSheetState.Origin.MessageDetails(MessageId("msg-1"))
private val transitionsFromLoadingState = listOf(
TestInput(
currentState = BottomSheetState(ContactActionsBottomSheetState.Loading),
@@ -76,7 +79,12 @@ internal class ContactActionsBottomSheetReducerTest(
contentState = ContactActionsBottomSheetState.Data(
participant = sampleParticipant,
avatarUiModel = sampleAvatar,
actions = ContactActionsGroupsSample.defaultForContact(sampleParticipant, false),
actions = expectedGroupsFor(
participant = sampleParticipant,
contactId = sampleContact.id,
isSenderBlocked = false,
isPrimaryUserAddress = false
),
origin = sampleOrigin
)
)
@@ -95,7 +103,12 @@ internal class ContactActionsBottomSheetReducerTest(
contentState = ContactActionsBottomSheetState.Data(
participant = sampleParticipant,
avatarUiModel = sampleAvatar,
actions = ContactActionsGroupsSample.defaultForContact(sampleParticipant, true),
actions = expectedGroupsFor(
participant = sampleParticipant,
contactId = sampleContact.id,
isSenderBlocked = true,
isPrimaryUserAddress = false
),
origin = sampleOrigin
)
)
@@ -114,7 +127,12 @@ internal class ContactActionsBottomSheetReducerTest(
contentState = ContactActionsBottomSheetState.Data(
participant = sampleParticipant,
avatarUiModel = sampleAvatar,
actions = ContactActionsGroupsSample.defaultForNoContact(sampleParticipant, true),
actions = expectedGroupsFor(
participant = sampleParticipant,
contactId = null,
isSenderBlocked = true,
isPrimaryUserAddress = false
),
origin = sampleOrigin
)
)
@@ -133,7 +151,12 @@ internal class ContactActionsBottomSheetReducerTest(
contentState = ContactActionsBottomSheetState.Data(
participant = sampleParticipant,
avatarUiModel = sampleAvatar,
actions = ContactActionsGroupsSample.defaultForNoContact(sampleParticipant, false),
actions = expectedGroupsFor(
participant = sampleParticipant,
contactId = null,
isSenderBlocked = false,
isPrimaryUserAddress = false
),
origin = sampleOrigin
)
)
@@ -152,8 +175,11 @@ internal class ContactActionsBottomSheetReducerTest(
contentState = ContactActionsBottomSheetState.Data(
participant = primaryUserParticipant,
avatarUiModel = sampleAvatar,
actions = ContactActionsGroupsSample.defaultForNoContact(
primaryUserParticipant, isAddressBlocked = false, isPrimaryUserAddress = true
actions = expectedGroupsFor(
participant = primaryUserParticipant,
contactId = null,
isSenderBlocked = false,
isPrimaryUserAddress = true
),
origin = sampleOrigin
)
@@ -161,6 +187,44 @@ internal class ContactActionsBottomSheetReducerTest(
)
)
private fun expectedGroupsFor(
participant: Participant,
contactId: ContactId?,
isSenderBlocked: Boolean,
isPrimaryUserAddress: Boolean
): ContactActionsBottomSheetState.ContactActionsGroups {
val first = listOf(
ContactActionUiModel.NewMessage(participant)
).toImmutableList()
val second = listOf(
ContactActionUiModel.CopyAddress(participant.address),
ContactActionUiModel.CopyName(participant.name)
).toImmutableList()
val third = if (isPrimaryUserAddress) {
emptyList()
} else {
listOf(
when {
contactId == null && !isSenderBlocked -> ContactActionUiModel.BlockAddress(participant)
contactId == null && isSenderBlocked -> ContactActionUiModel.UnblockAddress(participant)
contactId != null && !isSenderBlocked -> ContactActionUiModel.BlockContact(
participant, contactId
)
else -> ContactActionUiModel.UnblockContact(participant)
}
)
}.toImmutableList()
return ContactActionsBottomSheetState.ContactActionsGroups(
firstGroup = first,
secondGroup = second,
thirdGroup = third
)
}
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun data() = transitionsFromLoadingState
@@ -45,7 +45,7 @@ object ContactActionsGroupsSample {
if (isContactBlocked) {
add(ContactActionUiModel.UnblockContact(participant))
} else {
add(ContactActionUiModel.BlockContact(participant))
add(ContactActionUiModel.BlockContact(participant, ContactIdSample.Doe))
}
}.toImmutableList()