diff --git a/app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt b/app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt index c6889c47c0c..de5bd2af1db 100644 --- a/app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt +++ b/app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt @@ -322,6 +322,10 @@ object ApiRepository : ApiRepositoryCore() { return callApi(ApiRoutes.resource(unscheduleDraftUrl), DELETE) } + suspend fun unsendMessage(unsendMessageUrl: String): ApiResponse { + return callApi(ApiRoutes.resource(unsendMessageUrl), PUT) + } + suspend fun rescheduleDraft(draftResource: String, scheduleDate: Date): ApiResponse { return callApi(ApiRoutes.rescheduleDraft(draftResource, scheduleDate), PUT) } diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ThreadController.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ThreadController.kt index 4641d646e6b..fd5346be56a 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ThreadController.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ThreadController.kt @@ -358,7 +358,7 @@ class ThreadController @Inject constructor(private val mailboxContentRealm: Real // If we've already got this Message's Draft beforehand, we need to save // its `draftLocalUuid`, otherwise we'll lose the link between them. private fun Message.getDraftLocalUuidBlocking(realm: TypedRealm): String? { - return if (isDraft) DraftController.getDraftByMessageUidBlocking(uid, realm)?.localUuid else null + return if (isDraft && !isScheduledMessage) DraftController.getDraftByMessageUidBlocking(uid, realm)?.localUuid else null } fun deleteSearchThreads(realm: MutableRealm) = with(realm) { diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/refreshStrategies/ThreadRecomputations.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/refreshStrategies/ThreadRecomputations.kt index 152f7f898c3..80859080ac5 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/refreshStrategies/ThreadRecomputations.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/refreshStrategies/ThreadRecomputations.kt @@ -103,7 +103,7 @@ object ThreadRecomputations { if (!message.isSeen) unseenMessagesCount++ from += message.from to += message.to - if (message.isDraft) hasDrafts = true + if (message.isDraft && !message.isScheduledMessage) hasDrafts = true if (message.isFavorite) isFavorite = true if (message.isAnswered) { isAnswered = true diff --git a/app/src/main/java/com/infomaniak/mail/data/models/draft/SendDraftResult.kt b/app/src/main/java/com/infomaniak/mail/data/models/draft/SendDraftResult.kt index 4497c7146c6..bd6ee59dab7 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/draft/SendDraftResult.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/draft/SendDraftResult.kt @@ -24,4 +24,7 @@ import kotlinx.serialization.Serializable data class SendDraftResult( @SerialName("etop") val scheduledMessageEtop: String, + + @SerialName("cancel_resource") + val cancelResourceUrl: String?, ) diff --git a/app/src/main/java/com/infomaniak/mail/data/models/message/Message.kt b/app/src/main/java/com/infomaniak/mail/data/models/message/Message.kt index 407175e14a6..3d2b4ccc2be 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/message/Message.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/message/Message.kt @@ -432,7 +432,7 @@ class Message : RealmObject, Snoozable { fun hasUnreadContent() = !isSeen || emojiReactions.any { !it.isSeen } - fun shouldBeExpanded(index: Int, lastIndex: Int) = !isDraft && (hasUnreadContent() || index == lastIndex) + fun shouldBeExpanded(index: Int, lastIndex: Int) = (!isDraft || isScheduledMessage) && (hasUnreadContent() || index == lastIndex) fun toThread() = Thread().apply { uid = this@Message.uid diff --git a/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt b/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt index d18cb8831f5..0091c9ad980 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt @@ -106,7 +106,9 @@ import java.util.Locale import java.util.UUID import javax.inject.Inject import kotlin.coroutines.resume -import com.infomaniak.core.legacy.R as RCore +import kotlin.math.max +import com.infomaniak.core.common.R as RCore +import com.infomaniak.core.legacy.R as RCoreLegacy @AndroidEntryPoint class MainActivity : BaseActivity() { @@ -351,7 +353,10 @@ class MainActivity : BaseActivity() { } } DraftAction.SEND, DraftAction.SEND_REACTION -> { - showSentDraftSnackbar() + // Waits 2s after cancel delay to guarantee the send action is committed + mainViewModel.refreshFoldersAfterSendDelay(localSettings.cancelDelay + 2) + val cancelResourceUrl = getString(DraftsActionsWorker.CANCEL_RESOURCE_URL_KEY) + showSentDraftSnackbar(cancelResourceUrl) } DraftAction.SCHEDULE -> { val scheduleDate = getString(DraftsActionsWorker.SCHEDULED_DRAFT_DATE_KEY) @@ -389,9 +394,20 @@ class MainActivity : BaseActivity() { } // Still display the Snackbar even if it took three times 10 seconds of timeout to succeed - private fun showSentDraftSnackbar() { + private fun showSentDraftSnackbar(cancelResourceUrl: String?) { showSendingSnackbarTimer.cancel() - snackbarManager.setValue(getString(R.string.snackbarEmailSent)) + if (cancelResourceUrl == null) { + snackbarManager.setValue(getString(R.string.snackbarEmailSent)) + } else { + snackbarManager.setValue( + title = getString(R.string.snackbarEmailSent), + buttonTitle = RCore.string.buttonCancel, + customBehavior = { mainViewModel.unsendMessage(cancelResourceUrl) }, + // Snackbar displays for 2 seconds less than actual cancel delay + // to ensure the user sees the snackbar disappear before the action is committed + length = max(0, (localSettings.cancelDelay - 2) * 1000) + ) + } } // Still display the Snackbar even if it took three times 10 seconds of timeout to succeed @@ -555,7 +571,7 @@ class MainActivity : BaseActivity() { trackInAppUpdateEvent(if (isWantingUpdate) MatomoName.DiscoverNow else MatomoName.DiscoverLater) }, onInstallStart = { trackInAppUpdateEvent(MatomoName.InstallUpdate) }, - onInstallFailure = { snackbarManager.setValue(getString(RCore.string.errorUpdateInstall)) }, + onInstallFailure = { snackbarManager.setValue(getString(RCoreLegacy.string.errorUpdateInstall)) }, onInAppUpdateUiChange = { isUpdateDownloaded -> SentryLog.d(APP_UPDATE_TAG, "Must display update button : $isUpdateDownloaded") mainViewModel.canInstallUpdate.value = isUpdateDownloaded diff --git a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt index d1c78e4d33e..adb2a621640 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt @@ -852,6 +852,31 @@ class MainViewModel @Inject constructor( showUnscheduledDraftSnackbar(apiResponse) } + fun unsendMessage(unsendMessageUrl: String) = viewModelScope.launch(ioCoroutineContext) { + val apiResponse = ApiRepository.unsendMessage(unsendMessageUrl) + if (apiResponse.isSuccess()) { + snackbarManager.postValue(appContext.getString(R.string.snackbarSendCancelled)) + val draftsFolderId = folderController.getFolder(FolderRole.DRAFT)?.id ?: return@launch + + currentMailbox.value?.let { mailbox -> + refreshFoldersAsync(mailbox, ImpactedFolders(mutableSetOf(draftsFolderId))) + } + } else { + snackbarManager.postValue(appContext.getString(R.string.errorSnoozeFailedCancel)) + } + } + + fun refreshFoldersAfterSendDelay(sendDelay: Int) = viewModelScope.launch(ioCoroutineContext) { + delay(sendDelay * 1_000L) + currentMailbox.value?.let { mailbox -> + refreshFoldersAsync( + mailbox, + ImpactedFolders(mutableSetOf(FolderRole.DRAFT, FolderRole.SENT)), + destinationFolderId = folderController.getFolder(FolderRole.SENT)?.id, + ) + } + } + private fun showUnscheduledDraftSnackbar(apiResponse: ApiResponse) { fun openDraftFolder() = viewModelScope.launch { folderController.getFolder(FolderRole.DRAFT)?.id?.let(::openFolder) } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/SnackbarManager.kt b/app/src/main/java/com/infomaniak/mail/ui/main/SnackbarManager.kt index 1172dda4aff..67f31333149 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/SnackbarManager.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/SnackbarManager.kt @@ -42,7 +42,7 @@ class SnackbarManager @Inject constructor() { onUndoData: ((data: UndoData) -> Unit)? = null, ) = with(snackbarFeedback) { removeObservers(activity) - observe(activity) { (title, undoData, buttonTitleRes, customBehavior) -> + observe(activity) { (title, undoData, buttonTitleRes, customBehavior, length) -> val action: (() -> Unit)? = if (undoData != null) { { onUndoData?.invoke(undoData) } } else { @@ -59,6 +59,7 @@ class SnackbarManager @Inject constructor() { anchor = getAnchor?.invoke(), actionButtonTitle = buttonTitle, onActionClicked = safeAction, + length = length, ) } } @@ -75,12 +76,24 @@ class SnackbarManager @Inject constructor() { } } - fun setValue(title: String, undoData: UndoData? = null, buttonTitle: Int? = null, customBehavior: (() -> Unit)? = null) { - snackbarFeedback.value = SnackbarData(title, undoData, buttonTitle, customBehavior) + fun setValue( + title: String, + undoData: UndoData? = null, + buttonTitle: Int? = null, + customBehavior: (() -> Unit)? = null, + length: Int = Snackbar.LENGTH_LONG, + ) { + snackbarFeedback.value = SnackbarData(title, undoData, buttonTitle, customBehavior, length) } - fun postValue(title: String, undoData: UndoData? = null, buttonTitle: Int? = null, customBehavior: (() -> Unit)? = null) { - snackbarFeedback.postValue(SnackbarData(title, undoData, buttonTitle, customBehavior)) + fun postValue( + title: String, + undoData: UndoData? = null, + buttonTitle: Int? = null, + customBehavior: (() -> Unit)? = null, + length: Int = Snackbar.LENGTH_LONG, + ) { + snackbarFeedback.postValue(SnackbarData(title, undoData, buttonTitle, customBehavior, length)) } private data class SnackbarData( @@ -88,6 +101,7 @@ class SnackbarManager @Inject constructor() { val undoData: UndoData?, @StringRes val buttonTitle: Int?, val customBehavior: (() -> Unit)?, + val length: Int, ) data class UndoData( diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/settings/SettingsFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/settings/SettingsFragment.kt index 6d8ee054a58..e6a8d9da843 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/settings/SettingsFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/settings/SettingsFragment.kt @@ -185,6 +185,12 @@ class SettingsFragment : Fragment() { settingsThreadMode.setSubtitle(threadMode.localisedNameRes) settingsExternalContent.setSubtitle(externalContent.localisedNameRes) settingsAutomaticAdvance.setSubtitle(autoAdvanceMode.localisedNameRes) + val cancelDelaySubtitle = if (cancelDelay == 0) { + getString(R.string.settingsDisabled) + } else { + getString(R.string.settingsDelaySeconds, cancelDelay) + } + settingsCancellationPeriod.setSubtitle(cancelDelaySubtitle) lifecycleScope.launch { UserDatabase().userDao().allUsers.map { list -> list.any { it.isStaff } }.collectLatest { hasStaffAccount -> if (!hasStaffAccount) return@collectLatest @@ -238,6 +244,10 @@ class SettingsFragment : Fragment() { safelyAnimatedNavigation(SettingsFragmentDirections.actionSettingsToAutoAdvanceSettings(), currentClassName) } + settingsCancellationPeriod.setOnClickListener { + safelyAnimatedNavigation(SettingsFragmentDirections.actionSettingsToCancelDelaySetting(), currentClassName) + } + settingsThreadListDensity.setOnClickListener { safelyAnimatedNavigation(SettingsFragmentDirections.actionSettingsToThreadListDensitySetting(), currentClassName) } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/settings/send/CancelDelaySettingFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/settings/send/CancelDelaySettingFragment.kt index 4ceea04b676..f8bf476cb55 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/settings/send/CancelDelaySettingFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/settings/send/CancelDelaySettingFragment.kt @@ -26,7 +26,6 @@ import com.infomaniak.core.legacy.utils.safeBinding import com.infomaniak.mail.R import com.infomaniak.mail.data.LocalSettings import com.infomaniak.mail.databinding.FragmentCancelDelaySettingBinding -import com.infomaniak.mail.utils.extensions.notYetImplemented import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -49,8 +48,7 @@ class CancelDelaySettingFragment : Fragment() { checkInitialValue() binding.radioGroup.onItemCheckedListener { _, value, _ -> - notYetImplemented() - val seconds = value?.toInt() ?: throw NullPointerException("Radio button had no associated value") + val seconds = value?.toInt() ?: return@onItemCheckedListener localSettings.cancelDelay = seconds } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt index ffb1eb8322a..9a53362e198 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt @@ -332,7 +332,9 @@ class ThreadAdapter( } private fun initMapForNewMessage(message: Message, position: Int) = with(threadAdapterState) { - if (isExpandedMap[message.uid] == null) { + val wasScheduledMessageNowDraft = isExpandedMap[message.uid] != null && message.isDraft && !message.isScheduledMessage + + if (isExpandedMap[message.uid] == null || wasScheduledMessageNowDraft) { isExpandedMap[message.uid] = message.shouldBeExpanded(position, items.lastIndex) } @@ -431,7 +433,7 @@ class ThreadAdapter( private fun MessageViewHolder.bindHeader(message: Message) = with(binding) { val messageDate = message.displayDate.toDate() - if (message.isDraft) { + if (message.isDraft && !message.isScheduledMessage) { userAvatar.loadUserAvatar(AccountUtils.currentUser!!) expeditorName.apply { text = context.getString(R.string.messageIsDraftOption) @@ -485,7 +487,7 @@ class ThreadAdapter( isExpandedMap[message.uid] = false onExpandOrCollapseMessage(message, shouldTrack = true) } else { - if (message.isDraft) { + if (message.isDraft && !message.isScheduledMessage) { threadAdapterCallbacks?.onDraftClicked?.invoke(message) } else { isExpandedMap[message.uid] = true @@ -917,17 +919,18 @@ class ThreadAdapter( private fun ItemMessageBinding.setHeaderState(message: Message, isExpanded: Boolean) { deleteDraftButton.apply { - isVisible = message.isDraft + isVisible = message.isDraft && !message.isScheduledMessage setOnClickListener { threadAdapterCallbacks?.onDeleteDraftClicked?.invoke(message) } } replyButton.apply { - isVisible = isExpanded && message.isScheduledDraft.not() + isVisible = isExpanded && !message.isScheduledDraft && !message.isScheduledMessage setOnClickListener { threadAdapterCallbacks?.onReplyClicked?.invoke(message) } } menuButton.apply { - isVisible = isExpanded && message.isScheduledDraft.not() + isVisible = isExpanded && !message.isScheduledDraft && !message.isScheduledMessage setOnClickListener { threadAdapterCallbacks?.onMenuClicked?.invoke(message) } } + sendingProgressText.isVisible = message.isScheduledMessage recipient.text = if (isExpanded) getAllRecipientsFormatted(message) else context.formatSubject(message.subject) recipientChevron.isVisible = isExpanded @@ -1086,7 +1089,9 @@ class ThreadAdapter( // check for anything that doesn't need to handle bind with precision using a custom payload message.body?.value == oldMessage.message.body?.value && message.splitBody == oldMessage.message.splitBody && - message.shouldHideDivider == oldMessage.message.shouldHideDivider + message.shouldHideDivider == oldMessage.message.shouldHideDivider && + message.isDraft == oldMessage.message.isDraft && + message.isScheduledMessage == oldMessage.message.isScheduledMessage }) } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt index 8307b1a69c3..0f1062241ec 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt @@ -51,6 +51,7 @@ import com.infomaniak.mail.MatomoMail.trackExternalEvent import com.infomaniak.mail.MatomoMail.trackNewMessageEvent import com.infomaniak.mail.MatomoMail.trackSendingDraftEvent import com.infomaniak.mail.R +import com.infomaniak.mail.data.LocalSettings import com.infomaniak.mail.data.api.ApiRepository import com.infomaniak.mail.data.cache.RealmDatabase import com.infomaniak.mail.data.cache.mailboxContent.DraftController @@ -158,6 +159,7 @@ class NewMessageViewModel @Inject constructor( private val sharedUtils: SharedUtils, private val signatureUtils: SignatureUtils, private val snackbarManager: SnackbarManager, + private val localSettings: LocalSettings, @IoDispatcher private val ioDispatcher: CoroutineDispatcher, @MainDispatcher private val mainDispatcher: CoroutineDispatcher, ) : AndroidViewModel(application) { @@ -1005,6 +1007,8 @@ class NewMessageViewModel @Inject constructor( cc = ccLiveData.valueOrEmpty().toRealmList() bcc = bccLiveData.valueOrEmpty().toRealmList() + if (draftAction == DraftAction.SEND) delay = localSettings.cancelDelay + updateDraftAttachmentsWithLiveData( uiAttachments = attachmentsLiveData.valueOrEmpty(), step = "executeDraftActionWhenStopping (action = ${draftAction.name}) -> updateDraftFromLiveData", diff --git a/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt b/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt index bfe23ed1193..7bc3b8541f0 100644 --- a/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt +++ b/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt @@ -166,6 +166,7 @@ class DraftsActionsWorker @AssistedInject constructor( const val RESULT_USER_ID_KEY = "resultUserIdKey" const val SCHEDULED_DRAFT_DATE_KEY = "scheduledDraftDateKey" const val UNSCHEDULE_DRAFT_URL_KEY = "unscheduleDraftUrlKey" + const val CANCEL_RESOURCE_URL_KEY = "cancelResourceUrlKey" const val IS_SUCCESS = "isSuccess" const val EMOJI_SENT_STATUS = "emojiSentStatusKey" diff --git a/app/src/main/java/com/infomaniak/mail/workers/MailActionsManager.kt b/app/src/main/java/com/infomaniak/mail/workers/MailActionsManager.kt index b29f63d1b83..5f2c44a2312 100644 --- a/app/src/main/java/com/infomaniak/mail/workers/MailActionsManager.kt +++ b/app/src/main/java/com/infomaniak/mail/workers/MailActionsManager.kt @@ -46,6 +46,7 @@ import com.infomaniak.mail.utils.uploadAttachmentsWithMutex import com.infomaniak.mail.workers.DraftsActionsWorker.Companion.ALL_EMOJI_SENT_STATUS import com.infomaniak.mail.workers.DraftsActionsWorker.Companion.ASSOCIATED_MAILBOX_UUID_KEY import com.infomaniak.mail.workers.DraftsActionsWorker.Companion.BIGGEST_SCHEDULED_MESSAGES_ETOP_KEY +import com.infomaniak.mail.workers.DraftsActionsWorker.Companion.CANCEL_RESOURCE_URL_KEY import com.infomaniak.mail.workers.DraftsActionsWorker.Companion.EMOJI_SENT_STATUS import com.infomaniak.mail.workers.DraftsActionsWorker.Companion.ERROR_MESSAGE_RESID_KEY import com.infomaniak.mail.workers.DraftsActionsWorker.Companion.IS_SUCCESS @@ -104,6 +105,7 @@ class MailActionsManager( var trackedScheduledDraftDate: String? = null var trackedUnscheduledDraftUrl: String? = null var isTrackedDraftSuccess: Boolean? = null + var trackedUnSendDraftUrl: String? = null val drafts = draftController.getDraftsWithActions(mailboxContentRealm) SentryLog.d(TAG, "handleDraftsActions: ${drafts.count()} drafts to handle") @@ -148,6 +150,7 @@ class MailActionsManager( remoteUuidOfTrackedDraft = savedDraftUuid trackedUnscheduledDraftUrl = unscheduleDraftUrl isTrackedDraftSuccess = true + trackedUnSendDraftUrl = cancelResourceUrl } scheduledMessageEtop?.let(scheduledMessagesEtops::add) realmActionOnDraft?.let(realmActionsOnDraft::add) @@ -214,6 +217,7 @@ class MailActionsManager( trackedDraftErrorMessageResId, trackedScheduledDraftDate, trackedUnscheduledDraftUrl, + trackedUnSendDraftUrl, emojiSendResults, ) } @@ -227,6 +231,7 @@ class MailActionsManager( trackedDraftErrorMessageResId: Int?, trackedScheduledDraftDate: String?, trackedUnscheduledDraftUrl: String?, + trackedUnsendDraftUrl: String?, emojiSendResults: List, ): ListenableWorker.Result { @@ -257,6 +262,7 @@ class MailActionsManager( isSuccessPair, SCHEDULED_DRAFT_DATE_KEY to trackedScheduledDraftDate, UNSCHEDULE_DRAFT_URL_KEY to trackedUnscheduledDraftUrl, + CANCEL_RESOURCE_URL_KEY to trackedUnsendDraftUrl, ) } else { emptyData @@ -313,6 +319,7 @@ class MailActionsManager( var scheduledMessageEtop: String? = null var scheduleDraftAction: String? = null var savedDraftUuid: String? = null + var cancelResource: String? = null SentryDebug.addDraftBreadcrumbs(draft, step = "executeDraftAction (action = ${draft.action?.name.toString()})") @@ -332,6 +339,7 @@ class MailActionsManager( realmActionOnDraft = null, scheduledMessageEtop = null, unscheduleDraftUrl = null, + cancelResourceUrl = null, errorMessageResId = R.string.errorCorruptAttachment, savedDraftUuid = null, isSuccess = false, @@ -359,6 +367,7 @@ class MailActionsManager( isSuccess() -> { realmActionOnDraft = deleteDraftCallback(draft) scheduledMessageEtop = data?.scheduledMessageEtop + cancelResource = data?.cancelResourceUrl } error?.exception is SerializationException -> { realmActionOnDraft = deleteDraftCallback(draft) @@ -406,6 +415,7 @@ class MailActionsManager( realmActionOnDraft = realmActionOnDraft, scheduledMessageEtop = scheduledMessageEtop, unscheduleDraftUrl = scheduleDraftAction, + cancelResourceUrl = cancelResource, errorMessageResId = null, savedDraftUuid = savedDraftUuid, isSuccess = true, @@ -471,6 +481,7 @@ class MailActionsManager( val realmActionOnDraft: ((MutableRealm) -> Unit)?, val scheduledMessageEtop: String?, val unscheduleDraftUrl: String?, + val cancelResourceUrl: String?, val errorMessageResId: Int?, val savedDraftUuid: String?, val isSuccess: Boolean, diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 4a5cae87ca7..ef936d9aa84 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -131,6 +131,14 @@ app:title="@string/settingsAutoAdvanceTitle" tools:subtitle="@string/settingsAutoAdvanceListOfThreadsDescription" /> + + + + diff --git a/app/src/main/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml index 17866b587ba..c434ce70b61 100644 --- a/app/src/main/res/navigation/main_navigation.xml +++ b/app/src/main/res/navigation/main_navigation.xml @@ -308,6 +308,9 @@ + diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 05fc506a4f9..eea5f12938f 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -386,6 +386,7 @@ Videresendt besked Skjul samtalen Kladde + Sender… Denne besked vises i spam-mappen, fordi %s er på listen over blokerede afsendere. Denne besked betragtes som spam. Af sikkerhedsmæssige årsager anbefaler vi dig at aktivere dit spamfilter. Denne besked betragtes som spam. Vi anbefaler dig at flytte den til \"Spam\"-mappen. @@ -591,6 +592,7 @@ Din besked er blevet gemt i kladder. Beskeden vil blive sendt den %s Beskeden planlægges + Afsendelse annulleret Afsender succesfuldt sortlistet Afsendere succesfuldt sortlistet diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 360124a980d..d7081e312b3 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -386,6 +386,7 @@ Weitergeleitete Nachricht Unterhaltung verbergen Entwurf + Senden… Diese Nachricht erscheint im Spam-Ordner, weil %s auf der Liste der blockierten Absender steht. Diese Nachricht wird als Spam betrachtet. Um sicher zu gehen, empfehlen wir Ihnen, Ihren Spam-Filter zu aktivieren. Diese Nachricht wird als Spam betrachtet. Wir empfehlen Ihnen, sie in den Ordner \"Spam\" zu verschieben. @@ -591,6 +592,7 @@ Ihre Nachricht wurde als Entwurf gespeichert. Die Nachricht wird am %s verschickt Die Nachricht wird geplant + Senden abgebrochen Absender erfolgreich auf die schwarze Liste gesetzt Erfolgreich auf die schwarze Liste gesetzte Absender diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 863370057ab..e4cd4bd6072 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -386,6 +386,7 @@ Προωθημένο μήνυμα Απόκρυψη συνομιλίας Πρόχειρο + Αποστολή… Αυτό το μήνυμα εμφανίζεται στον φάκελο ανεπιθύμητης αλληλογραφίας επειδή ο %s βρίσκεται στη λίστα αποκλεισμένων αποστολέων. Αυτό το μήνυμα θεωρείται ανεπιθύμητο. Για ασφάλεια, σας συνιστούμε να ενεργοποιήσετε το φίλτρο ανεπιθύμητης αλληλογραφίας σας. Αυτό το μήνυμα θεωρείται ανεπιθύμητο. Σας συνιστούμε να το μετακινήσετε στο φάκελο \"Spam\". @@ -591,6 +592,7 @@ Το μήνυμα αποθηκεύτηκε στα πρόχειρα. Το μήνυμα θα σταλεί στις %s Το μήνυμα προγραμματίζεται + Αποστολή ακυρώθηκε Ο αποστολέας προστέθηκε επιτυχώς στη μαύρη λίστα Οι αποστολείς προστέθηκαν επιτυχώς στη μαύρη λίστα diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 473c45daa5d..2c56acc3b9d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -386,6 +386,7 @@ Mensaje reenviado Ocultar la conversación Borrador + Enviando… Este mensaje aparece en la carpeta de spam porque %s está en la lista de remitentes bloqueados. Este mensaje se considera spam. Para mayor seguridad, le recomendamos que active su filtro antispam. Este mensaje se considera spam. Le recomendamos que lo mueva a la carpeta \"Spam\". @@ -591,6 +592,7 @@ Su mensaje se ha guardado en borradores. El mensaje será enviado el %s El mensaje está siendo programado + Envío cancelado Remitente incluido en la lista negra Remitentes incluidos con éxito en la lista negra diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 946b4363d92..360a90bd0eb 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -386,6 +386,7 @@ Välitetty viesti Piilota keskustelu Luonnos + Lähetetään… Tämä viesti näkyy roskapostikansiossa, koska %s on estettyjen lähettäjien listalla. Tämä viesti katsotaan roskapostiksi. Turvallisuutesi vuoksi suosittelemme roskapostisuodattimen aktivointia. Tämä viesti katsotaan roskapostiksi. Suosittelemme siirtämään sen \"Roskaposti\"-kansioon. @@ -591,6 +592,7 @@ Viestisi on tallennettu luonnoksiin. Viesti lähetetään %s Viestiä ajastetaan + Lähetys peruttu Lähettäjä lisätty mustalle listalle onnistuneesti Lähettäjät lisätty mustalle listalle onnistuneesti diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 10bcddec91a..747dbb5ec5c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -386,6 +386,7 @@ Message transféré Cacher la conversation Brouillon + Envoi… Ce message figure dans les spam car %s se trouve dans la liste des expéditeurs bloqués. Ce message est considéré comme du spam. Par sécurité, nous vous recommandons d’activer le filtre anti-spam. Ce message est considéré comme du spam. Nous vous recommandons de le placer dans le dossier \"Spam\". @@ -591,6 +592,7 @@ Votre message a été sauvegardé dans les brouillons. Le message sera envoyé le %s Le message est en cours de planification + Envoi annulé Expéditeur blacklisté avec succès Expéditeurs blacklistés avec succès diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 8e799817973..3e5f2202de7 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -386,6 +386,7 @@ Messaggio inoltrato Nascondi la conversazione Bozza + Invio… Questo messaggio appare nella cartella spam perché %s è nell’elenco dei mittenti bloccati. Questo messaggio è considerato spam. Per sicurezza, si consiglia di attivare il filtro antispam. Questo messaggio è considerato spam. Si consiglia di spostarlo nella cartella \"Spam\". @@ -591,6 +592,7 @@ Il tuo messaggio è stato salvato nelle bozze. Il messaggio verrà inviato il %s Il messaggio è in fase di programmazione + Invio annullato Mittente inserito nella lista nera con successo Mittenti inseriti nella lista nera con successo diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index c71d44ee98e..c0747894729 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -386,6 +386,7 @@ Videresendt melding Skjul samtalen Utkast + Sender… Denne meldingen vises i spam-mappen fordi %s er på listen over blokkerte avsendere. Denne meldingen anses som spam. For sikkerhet anbefaler vi deg å aktivere spam-filteret. Denne meldingen anses som spam. Vi anbefaler deg å flytte den til \"Spam\"-mappen. @@ -591,6 +592,7 @@ Meldingen din er lagret i utkast. Meldingen vil bli sendt %s Meldingen planlegges + Sending avbrutt Avsender blokkert Avsendere blokkert diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 6a0c08db88c..27f5e42960c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -386,6 +386,7 @@ Doorgestuurd bericht Gesprek verbergen Concept + Verzenden… Dit bericht staat in de spammap omdat %s op de lijst met geblokkeerde afzenders staat. Dit bericht wordt als spam beschouwd. Voor uw veiligheid raden wij u aan uw spamfilter te activeren. Dit bericht wordt als spam beschouwd. Wij raden u aan het naar de \"Spam\"-map te verplaatsen. @@ -591,6 +592,7 @@ Uw bericht is opgeslagen in concepten. Het bericht wordt verzonden op %s Het bericht wordt gepland + Verzenden geannuleerd Afzender succesvol op de blacklist gezet Afzenders succesvol op de blacklist gezet diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 28356e37194..f6e102a59a0 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -402,6 +402,7 @@ Przekazana wiadomość Ukryj rozmowę Wersja robocza + Wysyłanie… Ta wiadomość pojawia się w folderze spam, ponieważ %s znajduje się na liście zablokowanych nadawców. Ta wiadomość jest uznawana za spam. W celu zapewnienia bezpieczeństwa zalecamy aktywację filtra antyspamowego. Ta wiadomość jest uznawana za spam. Zalecamy przeniesienie jej do folderu \"Spam\". @@ -615,6 +616,7 @@ Twoja wiadomość została zapisana w wersjach roboczych. Wiadomość zostanie wysłana %s Wiadomość jest planowana + Wysyłanie anulowane Nadawca pomyślnie dodany do czarnej listy Nadawcy pomyślnie dodani do czarnej listy diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 5fb59b74fb8..9e8c2096bf4 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -386,6 +386,7 @@ Mensagem reencaminhada Ocultar a conversa Rascunho + A enviar… Esta mensagem aparece na pasta spam porque %s está na lista de remetentes bloqueados. Esta mensagem é considerada spam. Por segurança, recomendamos que ative o filtro anti-spam. Esta mensagem é considerada spam. Recomendamos que a mova para a pasta \"Spam\". @@ -591,6 +592,7 @@ A sua mensagem foi guardada nos rascunhos. A mensagem será enviada em %s A programar a mensagem + Envio cancelado Remetente colocado na lista negra com sucesso Remetentes colocados na lista negra com sucesso diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 8f32f0e8a28..7b4b726c21e 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -386,6 +386,7 @@ Vidarebefordrat meddelande Dölj konversationen Utkast + Skickar… Detta meddelande visas i spam-mappen eftersom %s finns på listan över blockerade avsändare. Detta meddelande anses vara spam. För säkerhets skull rekommenderar vi dig att aktivera ditt spamfilter. Detta meddelande anses vara spam. Vi rekommenderar dig att flytta det till mappen \"Spam\". @@ -591,6 +592,7 @@ Ditt meddelande har sparats i utkast. Meddelandet kommer att skickas %s Meddelandet schemaläggs + Sändning avbruten Avsändaren har lagts till i svartlistan Avsändarna har lagts till i svartlistan diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7941c39685..39d505faafa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -390,6 +390,7 @@ Forwarded message Hide the conversation Draft + Sending… This message appears in the spam folder because %s is on the list of blocked senders. This message is considered to be spam. To be safe, we recommend you to activate your spam filter. This message is considered to be spam. We recommend you to move it to the \"Spam\" folder. @@ -595,6 +596,7 @@ Your message has been saved in drafts. The message will be sent on %s The message is being scheduled + Send cancelled Sender successfully blacklisted Senders successfully blacklisted diff --git a/app/src/test/java/com/infomaniak/mail/workers/MailActionsManagerTest.kt b/app/src/test/java/com/infomaniak/mail/workers/MailActionsManagerTest.kt index 24e45ad3f17..a2361f63224 100644 --- a/app/src/test/java/com/infomaniak/mail/workers/MailActionsManagerTest.kt +++ b/app/src/test/java/com/infomaniak/mail/workers/MailActionsManagerTest.kt @@ -141,7 +141,7 @@ class MailActionsManagerTest { coEvery { ApiRepository.sendDraft(any(), any(), any()) } coAnswers { ApiResponse( result = ApiResponseStatus.SUCCESS, - data = SendDraftResult(scheduledMessageEtop = "2025-08-27T10:45:48+0200"), + data = SendDraftResult(scheduledMessageEtop = "2025-08-27T10:45:48+0200", cancelResourceUrl = null), ) } coEvery { ApiRepository.createAttachments(any(), any(), any(), any()) } coAnswers {