mirror of
https://github.com/ProtonMail/android-mail.git
synced 2026-05-15 09:50:40 +00:00
UI for snooze message banner
ET-3986
This commit is contained in:
+11
-1
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -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
|
||||
|
||||
|
||||
+9
-1
@@ -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()
|
||||
}
|
||||
|
||||
+32
-18
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -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(
|
||||
|
||||
+6
-3
@@ -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 = { }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+10
-4
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+31
-2
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+19
-1
@@ -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>
|
||||
|
||||
+31
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+2
-1
@@ -66,7 +66,8 @@ internal class MessageDetailFooterActionsTest {
|
||||
onEditScheduleSendMessage = {},
|
||||
onRetryRsvpEventLoading = {},
|
||||
onAnswerRsvpEvent = { _, _ -> },
|
||||
onMessage = {}
|
||||
onMessage = {},
|
||||
onUnsnoozeMessage = {}
|
||||
)
|
||||
|
||||
// When
|
||||
|
||||
+7
-1
@@ -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(
|
||||
|
||||
+60
-1
@@ -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(
|
||||
|
||||
+7
-3
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user