UI for snooze message banner

ET-3986
This commit is contained in:
Seren
2025-07-30 17:25:26 +02:00
committed by MargeBot
parent 690aa9c17a
commit 67aa9bfaea
16 changed files with 240 additions and 39 deletions
@@ -23,6 +23,7 @@ import ch.protonmail.android.maildetail.presentation.model.AutoDeleteBannerUiMod
import ch.protonmail.android.maildetail.presentation.model.ExpirationBannerUiModel
import ch.protonmail.android.maildetail.presentation.model.MessageBannersUiModel
import ch.protonmail.android.maildetail.presentation.model.ScheduleSendBannerUiModel
import ch.protonmail.android.maildetail.presentation.model.SnoozeBannerUiModel
import ch.protonmail.android.maildetail.presentation.usecase.FormatScheduleSendTime
import ch.protonmail.android.mailmessage.domain.model.MessageBanner
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -39,7 +40,8 @@ class MessageBannersUiModelMapper @Inject constructor(
shouldShowBlockedSenderBanner = messageBanners.contains(MessageBanner.BlockedSender),
expirationBannerUiModel = toExpirationBannerUiModel(messageBanners),
autoDeleteBannerUiModel = toAutoDeleteBannerUiModel(messageBanners),
scheduleSendBannerUiModel = toScheduleSendBannerUiModel(messageBanners)
scheduleSendBannerUiModel = toScheduleSendBannerUiModel(messageBanners),
snoozeBannerUiModel = toSnoozeBannerUiModel(messageBanners)
)
private fun toExpirationBannerUiModel(messageBanners: List<MessageBanner>): ExpirationBannerUiModel {
@@ -62,4 +64,12 @@ class MessageBannersUiModelMapper @Inject constructor(
)
} ?: ScheduleSendBannerUiModel.NoScheduleSend
}
private fun toSnoozeBannerUiModel(messageBanners: List<MessageBanner>): SnoozeBannerUiModel {
return messageBanners.filterIsInstance<MessageBanner.Snoozed>().firstOrNull()?.let {
SnoozeBannerUiModel.SnoozeScheduled(
snoozedUntil = formatScheduleSendTime(it.snoozedUntil)
)
} ?: SnoozeBannerUiModel.NotSnoozed
}
}
@@ -168,6 +168,9 @@ sealed interface ConversationDetailEvent : ConversationDetailOperation {
ConversationDetailEvent, AffectingErrorBar, AffectingMessages
object ErrorAnsweringRsvpEvent : ConversationDetailEvent, AffectingErrorBar
object ErrorUnsnoozing : ConversationDetailEvent, AffectingErrorBar
}
sealed interface ConversationDetailViewAction : ConversationDetailOperation {
@@ -333,6 +336,9 @@ sealed interface ConversationDetailViewAction : ConversationDetailOperation {
data class EditScheduleSendMessageRequested(val messageId: MessageIdUiModel) :
ConversationDetailViewAction, AffectingEditScheduleMessageDialog
data class OnUnsnoozeConversationRequested(val messageId: MessageIdUiModel) :
ConversationDetailViewAction, AffectingBottomSheet
data class PrintMessage(val context: Context, val messageId: MessageId) :
ConversationDetailViewAction, AffectingBottomSheet
@@ -27,7 +27,8 @@ data class MessageBannersUiModel(
val shouldShowBlockedSenderBanner: Boolean,
val expirationBannerUiModel: ExpirationBannerUiModel,
val autoDeleteBannerUiModel: AutoDeleteBannerUiModel,
val scheduleSendBannerUiModel: ScheduleSendBannerUiModel
val scheduleSendBannerUiModel: ScheduleSendBannerUiModel,
val snoozeBannerUiModel: SnoozeBannerUiModel
)
sealed class ExpirationBannerUiModel {
@@ -51,3 +52,10 @@ sealed class ScheduleSendBannerUiModel {
val isScheduleBeingCancelled: Boolean
) : ScheduleSendBannerUiModel()
}
sealed class SnoozeBannerUiModel {
data object NotSnoozed : SnoozeBannerUiModel()
data class SnoozeScheduled(
val snoozedUntil: TextUiModel
) : SnoozeBannerUiModel()
}
@@ -28,18 +28,30 @@ import ch.protonmail.android.maildetail.domain.model.OpenProtonCalendarIntentVal
import ch.protonmail.android.maildetail.presentation.R
import ch.protonmail.android.maildetail.presentation.mapper.ActionResultMapper
import ch.protonmail.android.maildetail.presentation.model.ConversationDeleteState
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.HandleOpenProtonCalendarRequest
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.OfflineErrorCancellingScheduleSend
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ExitScreenWithMessage
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ExitScreen
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.LastMessageMoved
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.MessageMoved
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorDeletingMessage
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.MessageBottomSheetEvent
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ScheduleSendCancelled
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorUnsnoozing
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ConversationBottomBarEvent
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ConversationBottomSheetEvent
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorAddStar
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorMarkingAsRead
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorAttachmentDownloadInProgress
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorDeletingConversation
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorExpandingDecryptMessageError
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorAnsweringRsvpEvent
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorExpandingRetrieveMessageError
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorExpandingRetrievingMessageOffline
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorGettingAttachment
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorGettingAttachmentNotEnoughSpace
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorLabelingConversation
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorCancellingScheduleSend
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorMarkingAsUnread
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorMovingConversation
import ch.protonmail.android.maildetail.presentation.model.ConversationDetailEvent.ErrorMovingMessage
@@ -113,7 +125,7 @@ class ConversationDetailReducer @Inject constructor(
private fun ConversationDetailState.toOpenComposerEffectState(
operation: ConversationDetailOperation
): Effect<MessageIdUiModel> = when (operation) {
is ConversationDetailEvent.ScheduleSendCancelled -> Effect.of(operation.messageId)
is ScheduleSendCancelled -> Effect.of(operation.messageId)
else -> onExitWithNavigateToComposer
}
@@ -143,7 +155,7 @@ class ConversationDetailReducer @Inject constructor(
if (operation is ConversationDetailOperation.AffectingBottomSheet) {
val bottomSheetOperation = when (operation) {
is ConversationBottomSheetEvent -> operation.bottomSheetOperation
is ConversationDetailEvent.MessageBottomSheetEvent -> operation.bottomSheetOperation
is MessageBottomSheetEvent -> operation.bottomSheetOperation
is ConversationDetailViewAction.RequestContactActionsBottomSheet,
is ConversationDetailViewAction.RequestConversationMoreActionsBottomSheet,
is ConversationDetailViewAction.RequestMessageMoreActionsBottomSheet,
@@ -156,8 +168,8 @@ class ConversationDetailReducer @Inject constructor(
is ErrorLabelingConversation,
is ErrorAddStar,
is ErrorDeletingConversation,
is ConversationDetailEvent.ErrorDeletingMessage,
is ConversationDetailEvent.ErrorMarkingAsRead,
is ErrorDeletingMessage,
is ErrorMarkingAsRead,
is ErrorMarkingAsUnread,
is ErrorMovingMessage,
is ErrorMovingToTrash,
@@ -182,10 +194,11 @@ class ConversationDetailReducer @Inject constructor(
is ConversationDetailViewAction.LabelAsCompleted,
is ConversationDetailViewAction.MoveToCompleted,
is ConversationDetailViewAction.PrintMessage,
is ConversationDetailEvent.MessageMoved,
is ConversationDetailEvent.LastMessageMoved,
is ConversationDetailEvent.ExitScreen,
is ConversationDetailEvent.ExitScreenWithMessage -> BottomSheetOperation.Dismiss
is MessageMoved,
is LastMessageMoved,
is ExitScreen,
is ConversationDetailViewAction.OnUnsnoozeConversationRequested,
is ExitScreenWithMessage -> BottomSheetOperation.Dismiss
}
bottomSheetReducer.newStateFrom(bottomSheetState, bottomSheetOperation)
} else {
@@ -198,7 +211,7 @@ class ConversationDetailReducer @Inject constructor(
val textResource = when (operation) {
is ErrorAddStar -> R.string.error_star_operation_failed
is ErrorRemoveStar -> R.string.error_unstar_operation_failed
is ConversationDetailEvent.ErrorMarkingAsRead -> R.string.error_mark_as_read_failed
is ErrorMarkingAsRead -> R.string.error_mark_as_read_failed
is ErrorMarkingAsUnread -> R.string.error_mark_as_unread_failed
is ErrorMovingToTrash -> R.string.error_move_to_trash_failed
is ErrorMovingConversation -> R.string.error_move_conversation_failed
@@ -211,11 +224,12 @@ class ConversationDetailReducer @Inject constructor(
is ErrorGettingAttachmentNotEnoughSpace -> R.string.error_get_attachment_not_enough_memory
is ErrorAttachmentDownloadInProgress -> R.string.error_attachment_download_in_progress
is ErrorDeletingConversation -> R.string.error_delete_conversation_failed
is ConversationDetailEvent.ErrorDeletingMessage -> R.string.error_delete_message_failed
is ConversationDetailEvent.ErrorCancellingScheduleSend -> R.string.error_cancel_schedule_send_failed
is ConversationDetailEvent.OfflineErrorCancellingScheduleSend ->
is ErrorUnsnoozing -> R.string.snooze_sheet_error_unable_to_unsnooze
is ErrorDeletingMessage -> R.string.error_delete_message_failed
is ErrorCancellingScheduleSend -> R.string.error_cancel_schedule_send_failed
is OfflineErrorCancellingScheduleSend ->
R.string.offline_error_cancel_schedule_send_failed
is ConversationDetailEvent.ErrorAnsweringRsvpEvent -> R.string.rsvp_widget_error_answering
is ErrorAnsweringRsvpEvent -> R.string.rsvp_widget_error_answering
}
Effect.of(TextUiModel(textResource))
} else {
@@ -240,7 +254,7 @@ class ConversationDetailReducer @Inject constructor(
private fun ConversationDetailState.toExitState(operation: ConversationDetailOperation): Effect<Unit> =
when (operation) {
is ConversationDetailEvent.ExitScreen -> Effect.of(Unit)
is ExitScreen -> Effect.of(Unit)
is ConversationDetailViewAction.ReportPhishingConfirmed -> when (messagesState) {
is ConversationDetailsMessagesState.Data -> if (messagesState.messages.size > 1) {
exitScreenEffect
@@ -257,7 +271,7 @@ class ConversationDetailReducer @Inject constructor(
private fun ConversationDetailState.toExitWithMessageState(
operation: ConversationDetailOperation
): Effect<ActionResult> = when (operation) {
is ConversationDetailEvent.ExitScreenWithMessage -> {
is ExitScreenWithMessage -> {
val actionResult = actionResultMapper.toActionResult(operation.operation)
if (actionResult != null) {
Effect.of(actionResult)
@@ -266,7 +280,7 @@ class ConversationDetailReducer @Inject constructor(
}
}
is ConversationDetailEvent.LastMessageMoved -> {
is LastMessageMoved -> {
val actionResult = actionResultMapper.toActionResult(operation)
if (actionResult != null) {
Effect.of(actionResult)
@@ -316,7 +330,7 @@ class ConversationDetailReducer @Inject constructor(
private fun ConversationDetailState.toNewOpenProtonCalendarIntentFrom(
operation: ConversationDetailOperation
): Effect<OpenProtonCalendarIntentValues> = when (operation) {
is ConversationDetailEvent.HandleOpenProtonCalendarRequest -> Effect.of(operation.intent)
is HandleOpenProtonCalendarRequest -> Effect.of(operation.intent)
else -> openProtonCalendarIntent
}
@@ -34,6 +34,7 @@ import ch.protonmail.android.maildetail.presentation.model.MessageLocationUiMode
import ch.protonmail.android.maildetail.presentation.model.ParticipantUiModel
import ch.protonmail.android.maildetail.presentation.model.RsvpWidgetUiModel
import ch.protonmail.android.maildetail.presentation.model.ScheduleSendBannerUiModel
import ch.protonmail.android.maildetail.presentation.model.SnoozeBannerUiModel
import ch.protonmail.android.maillabel.domain.sample.LabelSample
import ch.protonmail.android.maillabel.presentation.model.LabelUiModel
import ch.protonmail.android.mailmessage.domain.model.Message
@@ -296,14 +297,16 @@ object ConversationDetailMessageUiModelSample {
)
private fun messageBannersUiModel(
scheduleSendBannerUiModel: ScheduleSendBannerUiModel = ScheduleSendBannerUiModel.NoScheduleSend
scheduleSendBannerUiModel: ScheduleSendBannerUiModel = ScheduleSendBannerUiModel.NoScheduleSend,
snoozeUiModel: SnoozeBannerUiModel = SnoozeBannerUiModel.NotSnoozed
) = MessageBannersUiModel(
shouldShowPhishingBanner = true,
shouldShowSpamBanner = false,
shouldShowBlockedSenderBanner = false,
expirationBannerUiModel = ExpirationBannerUiModel.NoExpiration,
autoDeleteBannerUiModel = AutoDeleteBannerUiModel.NoAutoDelete,
scheduleSendBannerUiModel = scheduleSendBannerUiModel
scheduleSendBannerUiModel = scheduleSendBannerUiModel,
snoozeBannerUiModel = snoozeUiModel
)
private fun buildExpanding(
@@ -292,7 +292,8 @@ private fun ColumnScope.ConversationDetailExpandedItem(
uiModel.messageDetailHeaderUiModel.sender.participantAddress
)
},
onCancelScheduleMessage = { actions.onEditScheduleSendMessage(uiModel.messageId) }
onCancelScheduleMessage = { actions.onEditScheduleSendMessage(uiModel.messageId) },
onUnsnoozeMessage = { actions.onUnsnoozeMessage(uiModel.messageId) }
)
MessageBody(
messageBodyUiModel = uiModel.messageBodyUiModel,
@@ -394,7 +395,8 @@ object ConversationDetailItem {
val onEditScheduleSendMessage: (MessageIdUiModel) -> Unit,
val onRetryRsvpEventLoading: (MessageIdUiModel) -> Unit,
val onAnswerRsvpEvent: (MessageIdUiModel, RsvpAnswer) -> Unit,
val onMessage: (String) -> Unit
val onMessage: (String) -> Unit,
val onUnsnoozeMessage: (MessageIdUiModel) -> Unit
)
val previewActions = Actions(
@@ -427,7 +429,8 @@ object ConversationDetailItem {
{ model: MessageIdUiModel -> },
{},
{ _, _ -> },
{}
{},
onUnsnoozeMessage = { }
)
}
@@ -525,7 +525,10 @@ fun ConversationDetailScreen(
onAnswerRsvpEvent = { messageId, answer ->
viewModel.submit(ConversationDetailViewAction.AnswerRsvpEvent(MessageId(messageId.id), answer))
},
onMessage = actions.onComposeNewMessage
onMessage = actions.onComposeNewMessage,
onUnsnoozeMessage = { messageId ->
viewModel.submit(ConversationDetailViewAction.OnUnsnoozeConversationRequested(messageId))
}
),
scrollToMessageId = state.scrollToMessage?.id
)
@@ -724,7 +727,8 @@ fun ConversationDetailScreen(
onEditScheduleSendMessage = actions.onEditScheduleSendMessage,
onRetryRsvpEventLoading = actions.onRetryRsvpEventLoading,
onAnswerRsvpEvent = actions.onAnswerRsvpEvent,
onMessage = actions.onMessage
onMessage = actions.onMessage,
onUnsnoozeMessage = actions.onUnsnoozeMessage
)
MessagesContentWithHiddenEdges(
uiModels = state.messagesState.messages,
@@ -1092,7 +1096,8 @@ object ConversationDetailScreen {
val onExitWithOpenInComposer: (MessageIdUiModel) -> Unit,
val onRetryRsvpEventLoading: (MessageIdUiModel) -> Unit,
val onAnswerRsvpEvent: (MessageIdUiModel, RsvpAnswer) -> Unit,
val onMessage: (String) -> Unit
val onMessage: (String) -> Unit,
val onUnsnoozeMessage: (MessageIdUiModel) -> Unit
) {
companion object {
@@ -1146,7 +1151,8 @@ object ConversationDetailScreen {
onExitWithOpenInComposer = {},
onRetryRsvpEventLoading = {},
onAnswerRsvpEvent = { _, _ -> },
onMessage = {}
onMessage = {},
onUnsnoozeMessage = {}
)
}
}
@@ -29,6 +29,7 @@ import ch.protonmail.android.maildetail.presentation.model.AutoDeleteBannerUiMod
import ch.protonmail.android.maildetail.presentation.model.ExpirationBannerUiModel
import ch.protonmail.android.maildetail.presentation.model.MessageBannersUiModel
import ch.protonmail.android.maildetail.presentation.model.ScheduleSendBannerUiModel
import ch.protonmail.android.maildetail.presentation.model.SnoozeBannerUiModel
import ch.protonmail.android.maildetail.presentation.util.toFormattedAutoDeleteTime
import ch.protonmail.android.maildetail.presentation.util.toFormattedExpirationTime
import kotlinx.coroutines.delay
@@ -42,7 +43,8 @@ fun MessageBanners(
messageBannersUiModel: MessageBannersUiModel,
onMarkMessageAsLegitimate: (Boolean) -> Unit,
onUnblockSender: () -> Unit,
onCancelScheduleMessage: () -> Unit
onCancelScheduleMessage: () -> Unit,
onUnsnoozeMessage: () -> Unit
) {
Column {
if (messageBannersUiModel.shouldShowPhishingBanner) {
@@ -85,6 +87,10 @@ fun MessageBanners(
ScheduleSendBannerUiModel.NoScheduleSend -> Unit
is ScheduleSendBannerUiModel.SendScheduled -> ScheduleSendBanner(uiModel, onCancelScheduleMessage)
}
when (val uiModel = messageBannersUiModel.snoozeBannerUiModel) {
SnoozeBannerUiModel.NotSnoozed -> Unit
is SnoozeBannerUiModel.SnoozeScheduled -> SnoozeBanner(uiModel, onUnsnoozeMessage)
}
if (messageBannersUiModel.shouldShowBlockedSenderBanner) {
ProtonBannerWithButton(
bannerText = stringResource(R.string.message_blocked_sender_banner_text),
@@ -96,6 +102,25 @@ fun MessageBanners(
}
}
@Composable
private fun SnoozeBanner(uiModel: SnoozeBannerUiModel.SnoozeScheduled, onUnsnoozeMessage: () -> Unit) {
val bannerBaseText = stringResource(R.string.snooze_message_snoozed_until_banner_title)
val sendTimeFormatted = uiModel.snoozedUntil.string()
val bannerText = buildAnnotatedString {
append(bannerBaseText)
appendLine()
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(sendTimeFormatted)
}
}
ProtonBannerWithButton(
bannerText = bannerText,
buttonText = stringResource(R.string.snooze_message_unsnooze_banner_button),
icon = R.drawable.ic_proton_clock,
onButtonClicked = onUnsnoozeMessage
)
}
@Composable
private fun ScheduleSendBanner(uiModel: ScheduleSendBannerUiModel.SendScheduled, onCancelScheduleMessage: () -> Unit) {
val bannerBaseText = stringResource(R.string.schedule_message_sent_at_banner_title)
@@ -219,11 +244,15 @@ fun PreviewMessageBanners() {
scheduleSendBannerUiModel = ScheduleSendBannerUiModel.SendScheduled(
sendAt = TextUiModel.Text("tomorrow at 08:00"),
isScheduleBeingCancelled = false
),
snoozeBannerUiModel = SnoozeBannerUiModel.SnoozeScheduled(
snoozedUntil = TextUiModel.Text("tomorrow at 08:00")
)
),
onMarkMessageAsLegitimate = {},
onUnblockSender = {},
onCancelScheduleMessage = {}
onCancelScheduleMessage = {},
onUnsnoozeMessage = {}
)
}
}
@@ -128,6 +128,7 @@ import ch.protonmail.android.mailmessage.presentation.model.bottomsheet.MoveToBo
import ch.protonmail.android.mailsession.domain.usecase.ObservePrimaryUserId
import ch.protonmail.android.mailsettings.domain.usecase.privacy.ObservePrivacySettings
import ch.protonmail.android.mailsettings.domain.usecase.privacy.UpdateLinkConfirmationSetting
import ch.protonmail.android.mailsnooze.domain.SnoozeRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineDispatcher
@@ -204,7 +205,8 @@ class ConversationDetailViewModel @Inject constructor(
private val cancelScheduleSendMessage: CancelScheduleSendMessage,
private val printMessage: PrintMessage,
private val getRsvpEvent: GetRsvpEvent,
private val answerRsvpEvent: AnswerRsvpEvent
private val answerRsvpEvent: AnswerRsvpEvent,
private val snoozeRepository: SnoozeRepository
) : ViewModel() {
private val primaryUserId = observePrimaryUserId()
@@ -366,7 +368,9 @@ class ConversationDetailViewModel @Inject constructor(
is ConversationDetailViewAction.PrintMessage -> handlePrintMessage(action.context, action.messageId)
is ConversationDetailViewAction.RetryRsvpEventLoading ->
handleGetRsvpEvent(action.messageId, refresh = true)
is ConversationDetailViewAction.AnswerRsvpEvent -> handleAnswerRsvpEvent(action.messageId, action.answer)
is ConversationDetailViewAction.OnUnsnoozeConversationRequested -> handleUnsnoozeMessage(action.messageId)
}
}
@@ -1318,6 +1322,20 @@ class ConversationDetailViewModel @Inject constructor(
}
}
private fun handleUnsnoozeMessage(messageId: MessageIdUiModel) {
viewModelScope.launch {
snoozeRepository.unSnoozeConversation(
userId = primaryUserId.first(),
labelId = openedFromLocation,
conversationIds = listOf(conversationId)
).onLeft { error ->
emitNewStateFrom(ConversationDetailEvent.ErrorUnsnoozing)
}.onRight {
setOrRefreshMessageBody(messageId)
}
}
}
/**
* A helper function that allows to perform actions that eventually cause the user to leave the screen, while
* still making sure that observers are not being triggered during the execution of the action.
@@ -168,4 +168,6 @@
<string name="rsvp_widget_open_in_proton_calendar">Open in Proton Calendar</string>
<string name="rsvp_widget_copy_address">Copy address</string>
<string name="rsvp_widget_message">Message</string>
<string name="snooze_message_snoozed_until_banner_title">Snoozed until</string>
<string name="snooze_message_unsnooze_banner_button">Unsnooze</string>
</resources>
@@ -24,6 +24,7 @@ import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
import ch.protonmail.android.maildetail.presentation.model.AutoDeleteBannerUiModel
import ch.protonmail.android.maildetail.presentation.model.ExpirationBannerUiModel
import ch.protonmail.android.maildetail.presentation.model.ScheduleSendBannerUiModel
import ch.protonmail.android.maildetail.presentation.model.SnoozeBannerUiModel
import ch.protonmail.android.maildetail.presentation.usecase.FormatScheduleSendTime
import ch.protonmail.android.mailmessage.domain.model.MessageBanner
import io.mockk.every
@@ -189,4 +190,34 @@ class MessageBannersUiModelMapperTest {
// Then
assertEquals(ScheduleSendBannerUiModel.NoScheduleSend, result.scheduleSendBannerUiModel)
}
@Test
fun `should map to ui model with snooze banner when banners list contains it`() {
// Given
val expected = TextUiModel.Text("in the far future")
every { formatScheduleSendTime(Instant.DISTANT_FUTURE) } returns expected
// When
val result = messageBannersUiModelMapper.toUiModel(
listOf(MessageBanner.Snoozed(Instant.DISTANT_FUTURE))
)
// Then
assertEquals(
SnoozeBannerUiModel.SnoozeScheduled(expected),
result.snoozeBannerUiModel
)
}
@Test
fun `should map to ui model with no snooze banner when banners list does not contain it`() {
// When
val result = messageBannersUiModelMapper.toUiModel(
emptyList()
)
// Then
assertEquals(SnoozeBannerUiModel.NotSnoozed, result.snoozeBannerUiModel)
}
}
@@ -351,7 +351,8 @@ class ConversationDetailReducerTest(
)
),
ConversationDetailEvent.MessageMoved(MailLabelText("String")) affects listOf(BottomSheet, MessageBar),
ConversationDetailEvent.ErrorMovingMessage affects listOf(BottomSheet, ErrorBar)
ConversationDetailEvent.ErrorMovingMessage affects listOf(BottomSheet, ErrorBar),
ConversationDetailEvent.ErrorUnsnoozing affects listOf(ErrorBar)
)
@JvmStatic
@@ -66,7 +66,8 @@ internal class MessageDetailFooterActionsTest {
onEditScheduleSendMessage = {},
onRetryRsvpEventLoading = {},
onAnswerRsvpEvent = { _, _ -> },
onMessage = {}
onMessage = {},
onUnsnoozeMessage = {}
)
// When
@@ -189,6 +189,7 @@ import ch.protonmail.android.mailsession.domain.usecase.ObservePrimaryUserId
import ch.protonmail.android.mailsettings.domain.model.PrivacySettings
import ch.protonmail.android.mailsettings.domain.usecase.privacy.ObservePrivacySettings
import ch.protonmail.android.mailsettings.domain.usecase.privacy.UpdateLinkConfirmationSetting
import ch.protonmail.android.mailsnooze.domain.SnoozeRepository
import ch.protonmail.android.testdata.action.AvailableActionsTestData
import ch.protonmail.android.testdata.avatar.AvatarImageStatesTestData
import ch.protonmail.android.testdata.contact.ContactSample
@@ -275,6 +276,10 @@ class ConversationDetailViewModelIntegrationTest {
every { this@mockk() } returns flowOf(AvatarImageStatesTestData.SampleData1)
}
private val snoozeRepository = mockk<SnoozeRepository> {
coEvery { this@mockk.unSnoozeConversation(any(), any(), any()) } returns Unit.right()
}
// Privacy settings for link confirmation dialog
private val observePrivacySettings = mockk<ObservePrivacySettings> {
coEvery { this@mockk.invoke(any()) } returns flowOf(
@@ -2518,7 +2523,8 @@ class ConversationDetailViewModelIntegrationTest {
cancelScheduleSendMessage = cancelScheduleSendMessage,
printMessage = printMessage,
getRsvpEvent = getRsvpEvent,
answerRsvpEvent = answerRsvpEvent
answerRsvpEvent = answerRsvpEvent,
snoozeRepository = snoozeRepository
)
private fun aMessageAttachment(id: String): AttachmentMetadata = AttachmentMetadata(
@@ -121,6 +121,8 @@ import ch.protonmail.android.mailsession.domain.usecase.ObservePrimaryUserId
import ch.protonmail.android.mailsettings.domain.model.PrivacySettings
import ch.protonmail.android.mailsettings.domain.usecase.privacy.ObservePrivacySettings
import ch.protonmail.android.mailsettings.domain.usecase.privacy.UpdateLinkConfirmationSetting
import ch.protonmail.android.mailsnooze.domain.SnoozeRepository
import ch.protonmail.android.mailsnooze.domain.model.UnsnoozeError
import ch.protonmail.android.testdata.action.ActionUiModelTestData
import ch.protonmail.android.testdata.avatar.AvatarImageStatesTestData
import ch.protonmail.android.testdata.contact.ContactActionsGroupsSample
@@ -323,6 +325,10 @@ class ConversationDetailViewModelTest {
private val getRsvpEvent = mockk<GetRsvpEvent>()
private val answerRsvpEvent = mockk<AnswerRsvpEvent>()
private val snoozeRepository = mockk<SnoozeRepository> {
coEvery { this@mockk.unSnoozeConversation(any(), any(), any()) } returns Unit.right()
}
private val testDispatcher: TestDispatcher by lazy {
StandardTestDispatcher().apply { Dispatchers.setMain(this) }
}
@@ -373,7 +379,8 @@ class ConversationDetailViewModelTest {
cancelScheduleSendMessage = cancelScheduleSendMessage,
printMessage = printMessage,
getRsvpEvent = getRsvpEvent,
answerRsvpEvent = answerRsvpEvent
answerRsvpEvent = answerRsvpEvent,
snoozeRepository = snoozeRepository
)
}
@@ -1904,6 +1911,58 @@ class ConversationDetailViewModelTest {
return Pair(allCollapsed.map { it.messageId }, InvoiceWithLabelExpanded)
}
@Test
fun `given unsnoozed successfully then message body is refreshed`() = runTest {
// given
val labelId = LabelIdSample.AllMail
val messageId = MessageIdUiModel("Id")
every { savedStateHandle.get<String>(ConversationDetailScreen.OpenedFromLocationKey) } returns labelId.id
// when
viewModel.state.test {
initialStateEmitted()
viewModel.submit(ConversationDetailViewAction.OnUnsnoozeConversationRequested(messageId))
advanceUntilIdle()
// then
coVerify { snoozeRepository.unSnoozeConversation(userId, labelId, listOf(conversationId)) }
advanceUntilIdle()
val expected = MessageBodyTransformations.MessageDetailsDefaults.copy()
coVerify(exactly = 1) {
getDecryptedMessageBody(userId, MessageId(messageId.id), expected)
}
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `error message is emitted when unsnooze fails`() = runTest {
// given
coEvery {
snoozeRepository.unSnoozeConversation(any(), any(), any())
} returns UnsnoozeError.Other().left()
coEvery {
reducer.newStateFrom(
currentState = ConversationDetailState.Loading,
operation = ConversationDetailEvent.ErrorUnsnoozing
)
} returns ConversationDetailState.Loading.copy(
error = Effect.of(TextUiModel(string.snooze_sheet_error_unable_to_unsnooze))
)
// when
viewModel.state.test {
initialStateEmitted()
viewModel.submit(ConversationDetailViewAction.OnUnsnoozeConversationRequested(MessageIdUiModel("id")))
// then
assertEquals(TextUiModel(string.snooze_sheet_error_unable_to_unsnooze), awaitItem().error.consume())
cancelAndIgnoreRemainingEvents()
}
}
private fun setupLinkClickState(messageId: MessageIdUiModel, link: Uri) {
coEvery {
reducer.newStateFrom(
@@ -22,6 +22,7 @@ import ch.protonmail.android.maildetail.presentation.model.AutoDeleteBannerUiMod
import ch.protonmail.android.maildetail.presentation.model.ExpirationBannerUiModel
import ch.protonmail.android.maildetail.presentation.model.MessageBannersUiModel
import ch.protonmail.android.maildetail.presentation.model.ScheduleSendBannerUiModel
import ch.protonmail.android.maildetail.presentation.model.SnoozeBannerUiModel
import kotlin.time.Instant
@Suppress("LongParameterList")
@@ -33,7 +34,8 @@ object MessageBannersUiModelTestData {
shouldShowBlockedSenderBanner = true,
expirationBannerUiModel = ExpirationBannerUiModel.Expiration(Instant.DISTANT_FUTURE),
autoDeleteBannerUiModel = AutoDeleteBannerUiModel.AutoDelete(Instant.DISTANT_FUTURE),
scheduleSendUiModel = ScheduleSendBannerUiModel.NoScheduleSend
scheduleSendUiModel = ScheduleSendBannerUiModel.NoScheduleSend,
snoozeBannerUiModel = SnoozeBannerUiModel.NotSnoozed
)
fun build(
@@ -42,13 +44,15 @@ object MessageBannersUiModelTestData {
shouldShowBlockedSenderBanner: Boolean,
expirationBannerUiModel: ExpirationBannerUiModel,
autoDeleteBannerUiModel: AutoDeleteBannerUiModel,
scheduleSendUiModel: ScheduleSendBannerUiModel
scheduleSendUiModel: ScheduleSendBannerUiModel,
snoozeBannerUiModel: SnoozeBannerUiModel
) = MessageBannersUiModel(
shouldShowPhishingBanner = shouldShowPhishingBanner,
shouldShowSpamBanner = shouldShowSpamBanner,
shouldShowBlockedSenderBanner = shouldShowBlockedSenderBanner,
expirationBannerUiModel = expirationBannerUiModel,
autoDeleteBannerUiModel = autoDeleteBannerUiModel,
scheduleSendBannerUiModel = scheduleSendUiModel
scheduleSendBannerUiModel = scheduleSendUiModel,
snoozeBannerUiModel = snoozeBannerUiModel
)
}