From f8d39796d58229858a9077a4c92c2bce56ad0e17 Mon Sep 17 00:00:00 2001 From: TommyDL-Infomaniak Date: Thu, 23 Jan 2025 11:19:45 +0100 Subject: [PATCH 01/70] feat: Add scheduled emails --- .../java/com/infomaniak/mail/MatomoMail.kt | 6 +- .../com/infomaniak/mail/data/LocalSettings.kt | 1 + .../infomaniak/mail/data/api/ApiRepository.kt | 22 +- .../com/infomaniak/mail/data/api/ApiRoutes.kt | 17 +- .../mail/data/cache/RealmDatabase.kt | 2 +- .../cache/mailboxContent/FolderController.kt | 67 ++++-- .../cache/mailboxContent/RefreshController.kt | 10 +- .../cache/mailboxContent/ThreadController.kt | 14 +- .../mail/data/models/FeatureFlag.kt | 1 + .../com/infomaniak/mail/data/models/Folder.kt | 15 +- .../mail/data/models/draft/Draft.kt | 63 ++++- .../mail/data/models/draft/SendDraftResult.kt | 2 +- .../models/draft/SendScheduleDraftResult.kt | 30 +++ .../mail/data/models/mailbox/Mailbox.kt | 2 + .../mail/data/models/message/Message.kt | 4 + .../mail/data/models/thread/Thread.kt | 7 +- .../com/infomaniak/mail/ui/MainActivity.kt | 48 +++- .../com/infomaniak/mail/ui/MainViewModel.kt | 110 ++++++++- ...ConfirmScheduledDraftModificationDialog.kt | 116 +++++++++ ...electDateAndTimeForScheduledDraftDialog.kt | 205 ++++++++++++++++ .../ScheduleSendBottomSheetDialog.kt | 224 ++++++++++++++++++ .../UpgradeProductBottomSheetDialog.kt | 41 ++++ .../mail/ui/main/folder/ThreadListAdapter.kt | 3 +- .../mail/ui/main/folder/TwoPaneViewModel.kt | 20 +- .../main/menuDrawer/items/FolderViewHolder.kt | 2 +- .../mail/ui/main/thread/MessageAlertView.kt | 4 + .../mail/ui/main/thread/ThreadAdapter.kt | 29 ++- .../mail/ui/main/thread/ThreadFragment.kt | 69 +++++- .../ui/main/thread/actions/ActionItemView.kt | 58 +++-- .../actions/MailActionsBottomSheetDialog.kt | 6 +- .../MessageActionsBottomSheetDialog.kt | 2 +- .../actions/MultiSelectBottomSheetDialog.kt | 4 +- .../actions/ThreadActionsBottomSheetDialog.kt | 2 +- .../mail/ui/newMessage/NewMessageActivity.kt | 15 +- .../ui/newMessage/NewMessageEditorManager.kt | 2 +- .../mail/ui/newMessage/NewMessageFragment.kt | 55 ++++- .../mail/ui/newMessage/NewMessageViewModel.kt | 27 ++- .../mail/utils/MailDateFormatUtils.kt | 4 +- .../mail/utils/extensions/Extensions.kt | 29 ++- .../mail/workers/BaseCoroutineWorker.kt | 2 + .../mail/workers/DraftsActionsWorker.kt | 90 +++++-- .../res/drawable/ic_afternoon_schedule.xml | 52 ++++ app/src/main/res/drawable/ic_arrow_return.xml | 17 ++ .../main/res/drawable/ic_evening_schedule.xml | 16 ++ .../drawable/ic_last_schedule_selected.xml | 13 + .../main/res/drawable/ic_morning_schedule.xml | 37 +++ .../drawable/ic_morning_sunrise_schedule.xml | 55 +++++ app/src/main/res/drawable/ic_pen.xml | 18 ++ .../main/res/drawable/ic_schedule_send.xml | 20 ++ .../main/res/layout/bottom_sheet_account.xml | 6 +- .../res/layout/bottom_sheet_actions_menu.xml | 20 +- .../bottom_sheet_attachment_actions.xml | 6 +- .../layout/bottom_sheet_detailed_contact.xml | 6 +- app/src/main/res/layout/bottom_sheet_junk.xml | 6 +- .../res/layout/bottom_sheet_multi_select.xml | 6 +- .../res/layout/bottom_sheet_schedule_send.xml | 60 +++++ .../layout/bottom_sheet_upgrade_product.xml | 62 +++++ .../main/res/layout/cardview_thread_item.xml | 16 +- ...g_confirm_scheduled_draft_modification.xml | 27 +++ ...lect_date_and_time_for_scheduled_draft.xml | 81 +++++++ .../fragment_account_management_settings.xml | 4 +- .../main/res/layout/fragment_new_message.xml | 34 ++- .../res/layout/item_bottom_sheet_action.xml | 63 ++++- app/src/main/res/layout/item_message.xml | 35 ++- .../layout/view_contact_chip_context_menu.xml | 4 +- .../main/res/navigation/main_navigation.xml | 22 ++ .../res/navigation/new_message_navigation.xml | 22 ++ app/src/main/res/values-de/strings.xml | 33 +++ app/src/main/res/values-es/strings.xml | 34 +++ app/src/main/res/values-fr/strings.xml | 34 +++ app/src/main/res/values-it/strings.xml | 33 +++ app/src/main/res/values-night/colors.xml | 1 + app/src/main/res/values/attrs.xml | 6 +- app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 33 +++ 75 files changed, 2112 insertions(+), 201 deletions(-) create mode 100644 app/src/main/java/com/infomaniak/mail/data/models/draft/SendScheduleDraftResult.kt create mode 100644 app/src/main/java/com/infomaniak/mail/ui/alertDialogs/ConfirmScheduledDraftModificationDialog.kt create mode 100644 app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt create mode 100644 app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt create mode 100644 app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/UpgradeProductBottomSheetDialog.kt create mode 100644 app/src/main/res/drawable/ic_afternoon_schedule.xml create mode 100644 app/src/main/res/drawable/ic_arrow_return.xml create mode 100644 app/src/main/res/drawable/ic_evening_schedule.xml create mode 100644 app/src/main/res/drawable/ic_last_schedule_selected.xml create mode 100644 app/src/main/res/drawable/ic_morning_schedule.xml create mode 100644 app/src/main/res/drawable/ic_morning_sunrise_schedule.xml create mode 100644 app/src/main/res/drawable/ic_pen.xml create mode 100644 app/src/main/res/drawable/ic_schedule_send.xml create mode 100644 app/src/main/res/layout/bottom_sheet_schedule_send.xml create mode 100644 app/src/main/res/layout/bottom_sheet_upgrade_product.xml create mode 100644 app/src/main/res/layout/dialog_confirm_scheduled_draft_modification.xml create mode 100644 app/src/main/res/layout/dialog_select_date_and_time_for_scheduled_draft.xml diff --git a/app/src/main/java/com/infomaniak/mail/MatomoMail.kt b/app/src/main/java/com/infomaniak/mail/MatomoMail.kt index 54f5af12fc..92d2c9d1ff 100644 --- a/app/src/main/java/com/infomaniak/mail/MatomoMail.kt +++ b/app/src/main/java/com/infomaniak/mail/MatomoMail.kt @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -269,6 +269,10 @@ object MatomoMail : MatomoCore { trackEvent("settingsAutoAdvance", name) } + fun Fragment.trackScheduleSendEvent(name: String) { + trackEvent("scheduleSend", name) + } + // We need to invert this logical value to keep a coherent value for analytics because actions // conditions are inverted (ex: if the condition is `message.isSpam`, then we want to unspam) private fun Boolean.toMailActionValue() = (!this).toFloat() diff --git a/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt b/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt index 6e96b7184a..9c49f44182 100644 --- a/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt +++ b/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt @@ -69,6 +69,7 @@ class LocalSettings private constructor(context: Context) : SharedValues { var autoAdvanceNaturalThread by sharedValue("autoAdvanceNaturalThreadKey", AutoAdvanceMode.FOLLOWING_THREAD) var showWebViewOutdated by sharedValue("showWebViewOutdatedKey", true) var accessTokenApiCallRecord by sharedValue("accessTokenApiCallRecordKey", null) + var lastSelectedScheduleDate by sharedValue("lastSelectedSchedule", null) fun removeSettings() = sharedPreferences.transaction { clear() } 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 a43f35a862..4e7ac4e582 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 @@ -30,7 +30,6 @@ import com.infomaniak.lib.core.utils.FORMAT_FULL_DATE_WITH_HOUR import com.infomaniak.lib.core.utils.format import com.infomaniak.mail.data.LocalSettings.AiEngine import com.infomaniak.mail.data.models.* -import com.infomaniak.mail.data.models.AttachmentDisposition import com.infomaniak.mail.data.models.addressBook.AddressBooksResult import com.infomaniak.mail.data.models.ai.AiMessage import com.infomaniak.mail.data.models.ai.AiResult @@ -44,6 +43,7 @@ import com.infomaniak.mail.data.models.correspondent.Recipient import com.infomaniak.mail.data.models.draft.Draft import com.infomaniak.mail.data.models.draft.SaveDraftResult import com.infomaniak.mail.data.models.draft.SendDraftResult +import com.infomaniak.mail.data.models.draft.SendScheduleDraftResult import com.infomaniak.mail.data.models.getMessages.ActivitiesResult import com.infomaniak.mail.data.models.getMessages.GetMessagesByUidsResult import com.infomaniak.mail.data.models.getMessages.NewMessagesResult @@ -193,6 +193,18 @@ object ApiRepository : ApiRepositoryCore() { return draft.remoteUuid?.let(::putDraft) ?: run(::postDraft) } + fun sendScheduleDraft(mailboxUuid: String, draft: Draft, okHttpClient: OkHttpClient): ApiResponse { + + val body = getDraftBody(draft) + + fun postDraft(): ApiResponse = callApi(ApiRoutes.draft(mailboxUuid), POST, body, okHttpClient) + + fun putDraft(uuid: String): ApiResponse = + callApi(ApiRoutes.draft(mailboxUuid, uuid), PUT, body, okHttpClient) + + return draft.remoteUuid?.let(::putDraft) ?: run(::postDraft) + } + private fun getDraftBody(draft: Draft): String { val updatedDraft = if (draft.identityId == Draft.NO_IDENTITY.toString()) { // When we select no signature, we create a dummy signature with -1 (NO_IDENTITY) as identity ID. @@ -240,6 +252,14 @@ object ApiRepository : ApiRepositoryCore() { return callApi(ApiRoutes.draft(mailboxUuid, remoteDraftUuid), DELETE) } + fun deleteScheduleDraft(scheduleAction: String): ApiResponse { + return callApi(ApiRoutes.scheduleDraft(scheduleAction), DELETE) + } + + fun rescheduleDraft(draftResource: String, scheduleDate: Date): ApiResponse { + return callApi(ApiRoutes.rescheduleDraft(draftResource, scheduleDate), PUT) + } + fun getDraft(messageDraftResource: String): ApiResponse = callApi(ApiRoutes.resource(messageDraftResource), GET) fun addToFavorites(mailboxUuid: String, messagesUids: List): List> { diff --git a/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt b/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt index ec24bd4e08..89cc52f1cb 100644 --- a/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt +++ b/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt @@ -18,8 +18,12 @@ package com.infomaniak.mail.data.api import com.infomaniak.lib.core.BuildConfig.INFOMANIAK_API_V1 +import com.infomaniak.lib.core.utils.FORMAT_SCHEDULE_MAIL +import com.infomaniak.lib.core.utils.format import com.infomaniak.mail.BuildConfig.MAIL_API import com.infomaniak.mail.utils.Utils +import java.net.URLEncoder +import java.util.Date object ApiRoutes { @@ -127,11 +131,11 @@ object ApiRoutes { } fun folders(mailboxUuid: String): String { - return "${mailMailbox(mailboxUuid)}/folder" + return "${mailMailbox(mailboxUuid)}/folder?with=ik-static" } private fun folder(mailboxUuid: String, folderId: String): String { - return "${folders(mailboxUuid)}/$folderId" + return "${mailMailbox(mailboxUuid)}/folder/$folderId" } fun flushFolder(mailboxUuid: String, folderId: String): String { @@ -223,6 +227,15 @@ object ApiRoutes { return "${draft(mailboxUuid)}/$remoteDraftUuid" } + fun scheduleDraft(scheduleAction: String): String { + return "$MAIL_API$scheduleAction" + } + + fun rescheduleDraft(draftResource: String, scheduleDate: Date): String { + val formatedDate = scheduleDate.format(FORMAT_SCHEDULE_MAIL) + return "$MAIL_API$draftResource/schedule?schedule_date=${URLEncoder.encode(formatedDate, "UTF-8")}" + } + fun createAttachment(mailboxUuid: String): String { return "${draft(mailboxUuid)}/attachment" } diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/RealmDatabase.kt b/app/src/main/java/com/infomaniak/mail/data/cache/RealmDatabase.kt index 8ace97abbe..82c7153315 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/RealmDatabase.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/RealmDatabase.kt @@ -161,7 +161,7 @@ object RealmDatabase { //region Configurations versions const val USER_INFO_SCHEMA_VERSION = 2L const val MAILBOX_INFO_SCHEMA_VERSION = 7L - const val MAILBOX_CONTENT_SCHEMA_VERSION = 19L + const val MAILBOX_CONTENT_SCHEMA_VERSION = 20L //endregion //region Configurations names diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/FolderController.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/FolderController.kt index e5ae656aa1..2c04d793de 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/FolderController.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/FolderController.kt @@ -46,19 +46,34 @@ class FolderController @Inject constructor( //region Get data fun getMenuDrawerDefaultFoldersAsync(): Flow> { - return getFoldersQuery(mailboxContentRealm(), withoutType = FoldersType.CUSTOM, withoutChildren = true).asFlow() + return getFoldersQuery( + mailboxContentRealm(), + withoutTypes = listOf(FoldersType.CUSTOM), + withoutChildren = true, + visibleFoldersOnly = true, + ).asFlow() } fun getMenuDrawerCustomFoldersAsync(): Flow> { - return getFoldersQuery(mailboxContentRealm(), withoutType = FoldersType.DEFAULT, withoutChildren = true).asFlow() + return getFoldersQuery( + mailboxContentRealm(), + withoutTypes = listOf(FoldersType.DEFAULT), + withoutChildren = true, + visibleFoldersOnly = true, + ).asFlow() } fun getSearchFoldersAsync(): Flow> { - return getFoldersQuery(mailboxContentRealm(), withoutChildren = true).asFlow() + return getFoldersQuery(mailboxContentRealm(), withoutChildren = true, visibleFoldersOnly = true).asFlow() } fun getMoveFolders(): RealmResults { - return getFoldersQuery(mailboxContentRealm(), withoutType = FoldersType.DRAFT, withoutChildren = true).find() + return getFoldersQuery( + mailboxContentRealm(), + withoutTypes = listOf(FoldersType.SCHEDULED_DRAFTS, FoldersType.DRAFT), + withoutChildren = true, + visibleFoldersOnly = true, + ).find() } fun getFolder(id: String): Folder? { @@ -108,20 +123,25 @@ class FolderController @Inject constructor( remoteFolders.forEach { remoteFolder -> - getFolder(remoteFolder.id, realm = this)?.let { localFolder -> + val localFolder = getFolder(remoteFolder.id, realm = this) + + if (remoteFolder.role == FolderRole.SCHEDULED_DRAFTS && localFolder == null) remoteFolder.isDisplayed = false + + localFolder?.let { val collapseStateNeedsReset = remoteFolder.isRoot && remoteFolder.children.isEmpty() - val isCollapsed = if (collapseStateNeedsReset) false else localFolder.isCollapsed + val isCollapsed = if (collapseStateNeedsReset) false else it.isCollapsed remoteFolder.initLocalValues( - localFolder.lastUpdatedAt, - localFolder.cursor, - localFolder.unreadCountLocal, - localFolder.threads, - localFolder.oldMessagesUidsToFetch, - localFolder.newMessagesUidsToFetch, - localFolder.remainingOldMessagesToFetch, - localFolder.isHidden, + it.lastUpdatedAt, + it.cursor, + it.unreadCountLocal, + it.threads, + it.oldMessagesUidsToFetch, + it.newMessagesUidsToFetch, + it.remainingOldMessagesToFetch, + it.isDisplayed, + it.isHidden, isCollapsed, ) } @@ -134,6 +154,7 @@ class FolderController @Inject constructor( enum class FoldersType { DEFAULT, CUSTOM, + SCHEDULED_DRAFTS, DRAFT, } @@ -145,17 +166,21 @@ class FolderController @Inject constructor( //region Queries private fun getFoldersQuery( realm: TypedRealm, - withoutType: FoldersType? = null, + withoutTypes: List = emptyList(), withoutChildren: Boolean = false, + visibleFoldersOnly: Boolean = false, ): RealmQuery { val rootsQuery = if (withoutChildren) " AND $isRootFolder" else "" - val typeQuery = when (withoutType) { - FoldersType.DEFAULT -> " AND ${Folder.rolePropertyName} == nil" - FoldersType.CUSTOM -> " AND ${Folder.rolePropertyName} != nil" - FoldersType.DRAFT -> " AND ${Folder.rolePropertyName} != '${FolderRole.DRAFT.name}'" - null -> "" + val typeQuery = withoutTypes.joinToString(separator = "") { + when (it) { + FoldersType.DEFAULT -> " AND ${Folder.rolePropertyName} == nil" + FoldersType.CUSTOM -> " AND ${Folder.rolePropertyName} != nil" + FoldersType.SCHEDULED_DRAFTS -> " AND ${Folder.rolePropertyName} != '${FolderRole.SCHEDULED_DRAFTS.name}'" + FoldersType.DRAFT -> " AND ${Folder.rolePropertyName} != '${FolderRole.DRAFT.name}'" + } } - return realm.query("$isNotSearch${rootsQuery}${typeQuery}").sortFolders() + val visibilityQuery = if (visibleFoldersOnly) " AND ${Folder::isDisplayed.name} == true" else "" + return realm.query("${isNotSearch}${rootsQuery}${typeQuery}${visibilityQuery}").sortFolders() } private fun getFoldersQuery(exceptionsFoldersIds: List, realm: TypedRealm): RealmQuery { diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/RefreshController.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/RefreshController.kt index 4f0cc34821..edddfec00e 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/RefreshController.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/RefreshController.kt @@ -195,9 +195,10 @@ class RefreshController @Inject constructor( clearCallbacks() when (initialFolder.role) { - FolderRole.INBOX -> listOf(FolderRole.SENT, FolderRole.DRAFT) - FolderRole.SENT -> listOf(FolderRole.INBOX, FolderRole.DRAFT) - FolderRole.DRAFT -> listOf(FolderRole.INBOX, FolderRole.SENT) + FolderRole.INBOX -> listOf(FolderRole.SENT, FolderRole.DRAFT, FolderRole.SCHEDULED_DRAFTS) + FolderRole.SENT -> listOf(FolderRole.INBOX, FolderRole.DRAFT, FolderRole.SCHEDULED_DRAFTS) + FolderRole.DRAFT -> listOf(FolderRole.INBOX, FolderRole.SENT, FolderRole.SCHEDULED_DRAFTS) + FolderRole.SCHEDULED_DRAFTS -> listOf(FolderRole.INBOX, FolderRole.SENT, FolderRole.DRAFT) else -> emptyList() }.forEach { role -> scope.ensureActive() @@ -285,6 +286,7 @@ class RefreshController @Inject constructor( it.newMessagesUidsToFetch.addAll(activities.addedShortUids) it.unreadCountRemote = activities.unreadCountRemote it.lastUpdatedAt = Date().toRealmInstant() + if (it.role == FolderRole.SCHEDULED_DRAFTS) it.isDisplayed = it.threads.isNotEmpty() it.cursor = activities.cursor SentryDebug.addCursorBreadcrumb("fetchActivities", it, activities.cursor) } @@ -345,6 +347,8 @@ class RefreshController @Inject constructor( max(it.remainingOldMessagesToFetch - uidsToFetch.count(), 0) } } + + if (it.role == FolderRole.SCHEDULED_DRAFTS) it.isDisplayed = it.threads.isNotEmpty() } sendOrphanMessages(folder) 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 e25d97dcb2..9c7033cda2 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 @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,8 +58,8 @@ class ThreadController @Inject constructor( ) { //region Get data - fun getThreadsAsync(folder: Folder, filter: ThreadFilter = ThreadFilter.ALL): Flow> { - return getThreadsQuery(folder, filter).asFlow() + fun getThreadsAsync(folder: Folder, filter: ThreadFilter = ThreadFilter.ALL, sortOrder: Sort): Flow> { + return getThreadsQuery(folder, filter, sortOrder).asFlow() } fun getSearchThreadsAsync(): Flow> { @@ -217,11 +217,15 @@ class ThreadController @Inject constructor( return folder.threads.query(unseen).count() } - private fun getThreadsQuery(folder: Folder, filter: ThreadFilter = ThreadFilter.ALL): RealmQuery { + private fun getThreadsQuery( + folder: Folder, + filter: ThreadFilter = ThreadFilter.ALL, + sortOrder: Sort, + ): RealmQuery { val notFromSearch = "${Thread::isFromSearch.name} == false" val notLocallyMovedOut = " AND ${Thread::isLocallyMovedOut.name} == false" - val realmQuery = folder.threads.query(notFromSearch + notLocallyMovedOut).sort(Thread::date.name, Sort.DESCENDING) + val realmQuery = folder.threads.query(notFromSearch + notLocallyMovedOut).sort(Thread::date.name, sortOrder) return if (filter == ThreadFilter.ALL) { realmQuery diff --git a/app/src/main/java/com/infomaniak/mail/data/models/FeatureFlag.kt b/app/src/main/java/com/infomaniak/mail/data/models/FeatureFlag.kt index 48cb42bebd..aac8fc9ca3 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/FeatureFlag.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/FeatureFlag.kt @@ -21,4 +21,5 @@ package com.infomaniak.mail.data.models enum class FeatureFlag(val apiName: String) { AI("ai-mail-composer"), BIMI("bimi"), + SCHEDULE_SEND_DRAFT("schedule-send-draft"), } diff --git a/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt b/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt index 76514c37d7..2487bb59a4 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -92,6 +92,8 @@ class Folder : RealmObject, Cloneable { var roleOrder: Int = role?.order ?: CUSTOM_FOLDER_ROLE_ORDER @Transient var sortedName: String = name + @Transient + var isDisplayed: Boolean = true //endregion private val _parents by backlinks(Folder::children) @@ -123,6 +125,7 @@ class Folder : RealmObject, Cloneable { oldMessagesUidsToFetch: RealmList, newMessagesUidsToFetch: RealmList, remainingOldMessagesToFetch: Int, + isDisplayed: Boolean, isHidden: Boolean, isCollapsed: Boolean, ) { @@ -134,6 +137,7 @@ class Folder : RealmObject, Cloneable { this.oldMessagesUidsToFetch.addAll(oldMessagesUidsToFetch) this.newMessagesUidsToFetch.addAll(newMessagesUidsToFetch) this.remainingOldMessagesToFetch = remainingOldMessagesToFetch + this.isDisplayed = isDisplayed this.isHidden = isHidden this.isCollapsed = isCollapsed @@ -161,10 +165,11 @@ class Folder : RealmObject, Cloneable { val order: Int, val matomoValue: String, ) { - INBOX(R.string.inboxFolder, R.drawable.ic_drawer_inbox, 8, "inboxFolder"), - COMMERCIAL(R.string.commercialFolder, R.drawable.ic_promotions, 7, "commercialFolder"), - SOCIALNETWORKS(R.string.socialNetworksFolder, R.drawable.ic_social_media, 6, "socialNetworksFolder"), - SENT(R.string.sentFolder, R.drawable.ic_sent_messages, 5, "sentFolder"), + INBOX(R.string.inboxFolder, R.drawable.ic_drawer_inbox, 9, "inboxFolder"), + COMMERCIAL(R.string.commercialFolder, R.drawable.ic_promotions, 8, "commercialFolder"), + SOCIALNETWORKS(R.string.socialNetworksFolder, R.drawable.ic_social_media, 7, "socialNetworksFolder"), + SENT(R.string.sentFolder, R.drawable.ic_sent_messages, 6, "sentFolder"), + SCHEDULED_DRAFTS(R.string.scheduledMessagesFolder, R.drawable.ic_schedule_send, 5, "scheduledDraftsFolder"), DRAFT(R.string.draftFolder, R.drawable.ic_draft, 4, "draftFolder"), SPAM(R.string.spamFolder, R.drawable.ic_spam, 3, "spamFolder"), TRASH(R.string.trashFolder, R.drawable.ic_bin, 2, "trashFolder"), diff --git a/app/src/main/java/com/infomaniak/mail/data/models/draft/Draft.kt b/app/src/main/java/com/infomaniak/mail/data/models/draft/Draft.kt index 445754a260..c664d37739 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/draft/Draft.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/draft/Draft.kt @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,13 +27,16 @@ import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.serializers.RealmListKSerializer import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.PrimaryKey -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient -import kotlinx.serialization.UseSerializers +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* import java.util.UUID +@OptIn(ExperimentalSerializationApi::class) @Serializable class Draft : RealmObject { @@ -69,9 +72,29 @@ class Draft : RealmObject { var swissTransferUuid: String? = null /** - * This `delay` should NOT be removed. If we remove it, we won't receive any `etop` from the API when sending an Email. + * This `delay` should be set to 0 when `scheduleDate` is NOT set. + * Otherwise, if we remove it, we won't receive any `etop` from the API when sending an Email. */ - var delay: Int = 0 + @EncodeDefault(EncodeDefault.Mode.NEVER) + @Serializable(with = ConditionalIntSerializer::class) + var delay: Int? = 0 + set(value) { + scheduleDate = null + field = value + } + + /** + * The API requires that this field does not exist, except when scheduling a message. + * We use a custom serializer for this very reason. + */ + @SerialName("schedule_date") + @EncodeDefault(EncodeDefault.Mode.NEVER) + @Serializable(with = ConditionalStringSerializer::class) + var scheduleDate: String? = null + set(value) { + delay = null + field = value + } //endregion //region Local data (Transient) @@ -102,6 +125,7 @@ class Draft : RealmObject { enum class DraftAction(val apiCallValue: String, val matomoValue: String) { SAVE("save", "saveDraft"), SEND("send", "sendMail"), + SCHEDULE("schedule", "scheduleDraft"), } enum class DraftMode { @@ -118,3 +142,28 @@ class Draft : RealmObject { private val draftJson = Json(ApiController.json) { encodeDefaults = true } } } + +object ConditionalIntSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ConditionalIntField", PrimitiveKind.INT) + + override fun serialize(encoder: Encoder, value: Int?) { + if (shouldSerialize(value)) encoder.encodeInt(value ?: 0) + } + + override fun deserialize(decoder: Decoder): Int = decoder.decodeInt() + + private fun shouldSerialize(value: Int?): Boolean = value != null +} + + +object ConditionalStringSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ConditionalStringField", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: String?) { + if (shouldSerialize(value)) encoder.encodeString(value ?: "") + } + + override fun deserialize(decoder: Decoder): String = decoder.decodeString() + + private fun shouldSerialize(value: String?): Boolean = value != null +} 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 d788c53ada..01757292b0 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 @@ -23,5 +23,5 @@ import kotlinx.serialization.Serializable @Serializable data class SendDraftResult( @SerialName("etop") - val scheduledDate: String, + val etopScheduledDate: String, ) diff --git a/app/src/main/java/com/infomaniak/mail/data/models/draft/SendScheduleDraftResult.kt b/app/src/main/java/com/infomaniak/mail/data/models/draft/SendScheduleDraftResult.kt new file mode 100644 index 0000000000..04d7e0b80d --- /dev/null +++ b/app/src/main/java/com/infomaniak/mail/data/models/draft/SendScheduleDraftResult.kt @@ -0,0 +1,30 @@ +/* + * Infomaniak Mail - Android + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.infomaniak.mail.data.models.draft + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SendScheduleDraftResult( + val uuid: String, + @SerialName("schedule_action") + val scheduleAction: String, + val uid: String, +) diff --git a/app/src/main/java/com/infomaniak/mail/data/models/mailbox/Mailbox.kt b/app/src/main/java/com/infomaniak/mail/data/models/mailbox/Mailbox.kt index 3bdd03329d..6ec9a4c8c5 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/mailbox/Mailbox.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/mailbox/Mailbox.kt @@ -65,6 +65,8 @@ class Mailbox : RealmObject { @SerialName("unseen_messages") var unreadCountRemote: Int = 0 var aliases = realmListOf() + @SerialName("is_free") + var isFree: Boolean = false //endregion //region Local data (Transient) 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 c82bb2f5f6..8b281d8b00 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 @@ -80,6 +80,8 @@ class Message : RealmObject { var isDraft: Boolean = false @SerialName("draft_resource") var draftResource: String? = null + @SerialName("is_scheduled_draft") + var isScheduledDraft: Boolean = false var body: Body? = null @SerialName("has_attachments") var hasAttachments: Boolean = false @@ -99,6 +101,8 @@ class Message : RealmObject { @SerialName("has_unsubscribe_link") var hasUnsubscribeLink: Boolean? = null var bimi: Bimi? = null + @SerialName("schedule_action") + var scheduleAction: String? = null // TODO: Those are unused for now, but if we ever want to use them, we need to save them in `Message.keepHeavyData()`. // If we don't do it now, we'll probably forget to do it in the future. diff --git a/app/src/main/java/com/infomaniak/mail/data/models/thread/Thread.kt b/app/src/main/java/com/infomaniak/mail/data/models/thread/Thread.kt index 6c1be59b4a..ffd2422c2b 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/thread/Thread.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/thread/Thread.kt @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -95,6 +95,8 @@ class Thread : RealmObject { // It's only used to filter locally the Threads' list. @Transient var isLocallyMovedOut: Boolean = false + @Transient + var numberOfScheduledDrafts: Int = 0 //endregion private val _folders by backlinks(Folder::threads) @@ -135,6 +137,7 @@ class Thread : RealmObject { val shouldAddMessage = when (FolderController.getFolder(folderId, realm)?.role) { FolderRole.DRAFT -> newMessage.isDraft // In Draft folder: only add draft Messages. + FolderRole.SCHEDULED_DRAFTS -> newMessage.isScheduledDraft // In ScheduledDrafts folder: only add scheduled Messages. FolderRole.TRASH -> newMessage.isTrashed // In Trash folder: only add deleted Messages. else -> !newMessage.isTrashed // In other folders: only add non-deleted Messages. } @@ -188,6 +191,7 @@ class Thread : RealmObject { isAnswered = false isForwarded = false hasAttachable = false + numberOfScheduledDrafts = 0 } private fun updateThread() { @@ -210,6 +214,7 @@ class Thread : RealmObject { isAnswered = false } if (message.hasAttachable) hasAttachable = true + if (message.isScheduledDraft) numberOfScheduledDrafts++ } date = messages.last { it.folderId == folderId }.date 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 0ec1dde3cd..869f07d423 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt @@ -38,11 +38,9 @@ import androidx.navigation.fragment.NavHostFragment import androidx.work.Data import com.airbnb.lottie.LottieAnimationView import com.infomaniak.lib.core.MatomoCore.TrackerAction -import com.infomaniak.lib.core.utils.SentryLog +import com.infomaniak.lib.core.utils.* import com.infomaniak.lib.core.utils.Utils import com.infomaniak.lib.core.utils.Utils.toEnumOrThrow -import com.infomaniak.lib.core.utils.hasPermissions -import com.infomaniak.lib.core.utils.year import com.infomaniak.lib.stores.StoreUtils import com.infomaniak.lib.stores.StoreUtils.checkUpdateIsRequired import com.infomaniak.lib.stores.reviewmanagers.InAppReviewManager @@ -69,10 +67,12 @@ import com.infomaniak.mail.ui.main.search.SearchFragmentArgs import com.infomaniak.mail.ui.newMessage.NewMessageActivity import com.infomaniak.mail.ui.sync.SyncAutoConfigActivity import com.infomaniak.mail.utils.* +import com.infomaniak.mail.utils.MailDateFormatUtils.mostDetailedDate import com.infomaniak.mail.utils.UiUtils.progressivelyColorSystemBars import com.infomaniak.mail.utils.Utils.Shortcuts import com.infomaniak.mail.utils.Utils.openShortcutHelp import com.infomaniak.mail.utils.extensions.isUserAlreadySynchronized +import com.infomaniak.mail.workers.BaseCoroutineWorker.Companion.getLongOrNull import com.infomaniak.mail.workers.DraftsActionsWorker import dagger.hilt.android.AndroidEntryPoint import io.sentry.Sentry @@ -103,15 +103,24 @@ class MainActivity : BaseActivity() { (supportFragmentManager.findFragmentById(R.id.mainHostFragment) as NavHostFragment).navController } + private var draftAction: DraftAction? = null + private val showSendingSnackbarTimer: CountDownTimer by lazy { - Utils.createRefreshTimer(milliseconds = 1_000L) { snackbarManager.setValue(getString(R.string.snackbarEmailSending)) } + Utils.createRefreshTimer(milliseconds = 1_000L) { + snackbarManager.setValue( + getString(if (draftAction == DraftAction.SCHEDULE) R.string.snackbarScheduling else R.string.snackbarEmailSending) + ) + } } private val newMessageActivityResultLauncher = registerForActivityResult(StartActivityForResult()) { result -> - val draftAction = result.data?.getStringExtra(DRAFT_ACTION_KEY)?.let(DraftAction::valueOf) + draftAction = result.data?.getStringExtra(DRAFT_ACTION_KEY)?.let(DraftAction::valueOf) + if (draftAction == DraftAction.SEND) { showEasterXMas() showSendingSnackbarTimer.start() + } else if (draftAction == DraftAction.SCHEDULE) { + showSendingSnackbarTimer.start() } } @@ -282,6 +291,14 @@ class MainActivity : BaseActivity() { } } DraftAction.SEND -> showSentDraftSnackbar() + DraftAction.SCHEDULE -> { + val scheduleDate = getLongOrNull(DraftsActionsWorker.SCHEDULE_DATE_KEY) + val scheduleAction = getString(DraftsActionsWorker.SCHEDULE_ACTION_KEY) + + if (scheduleDate != null && scheduleAction != null) { + showSentScheduleDraftSnackbar(scheduleDate = Date(scheduleDate), scheduleAction) + } + } } } } @@ -290,8 +307,8 @@ class MainActivity : BaseActivity() { val userId = getInt(DraftsActionsWorker.RESULT_USER_ID_KEY, 0) if (userId != AccountUtils.currentUserId) return - getLong(DraftsActionsWorker.BIGGEST_SCHEDULED_DATE_KEY, 0).takeIf { it > 0 }?.let { scheduledDate -> - mainViewModel.refreshDraftFolderWhenDraftArrives(scheduledDate) + getLong(DraftsActionsWorker.BIGGEST_ETOP_SCHEDULED_DATE_KEY, 0).takeIf { it > 0 }?.let { etopScheduledDate -> + mainViewModel.refreshDraftFolderWhenDraftArrives(etopScheduledDate) } } @@ -312,6 +329,23 @@ class MainActivity : BaseActivity() { snackbarManager.setValue(getString(R.string.snackbarEmailSent)) } + // Still display the Snackbar even if it took three times 10 seconds of timeout to succeed + private fun showSentScheduleDraftSnackbar(scheduleDate: Date, scheduleAction: String) { + showSendingSnackbarTimer.cancel() + + val dateString = mostDetailedDate( + context = this, + date = scheduleDate, + format = FORMAT_DATE_DAY_MONTH, + ) + + snackbarManager.setValue( + title = String.format(getString(R.string.snackbarScheduleSaved), dateString), + buttonTitle = RCore.string.buttonCancel, + customBehavior = { mainViewModel.deleteScheduleDraft(scheduleAction) }, + ) + } + private fun loadCurrentMailbox() { mainViewModel.loadCurrentMailboxFromLocal().observe(this) { 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 d1a8f82c78..f9ad1811d4 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -70,6 +70,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import io.realm.kotlin.Realm import io.realm.kotlin.ext.toRealmList import io.realm.kotlin.notifications.ResultsChange +import io.realm.kotlin.query.Sort import io.sentry.Attachment import io.sentry.Sentry import io.sentry.SentryLevel @@ -202,7 +203,10 @@ class MainViewModel @Inject constructor( currentThreadsLiveJob = viewModelScope.launch(ioCoroutineContext) { observeFolderAndFilter() .flatMapLatest { (folder, filter) -> - folder?.let { threadController.getThreadsAsync(it, filter) } ?: emptyFlow() + folder?.let { + val sortOrder = if (folder.role == FolderRole.SCHEDULED_DRAFTS) Sort.ASCENDING else Sort.DESCENDING + threadController.getThreadsAsync(it, filter, sortOrder) + } ?: emptyFlow() } .collect(currentThreadsLive::postValue) } @@ -233,6 +237,14 @@ class MainViewModel @Inject constructor( val mergedContactsLive: LiveData = avatarMergedContactData.mergedContactLiveData //endregion + //region Schedule draft + var draftResource: String? = null + + val showOrCloseSelectDateAndTimeForScheduleDialog = SingleLiveEvent() + + fun showSelectDateAndTimeForScheduleDialog() = showOrCloseSelectDateAndTimeForScheduleDialog.postValue(Unit) + //endregion + //region Share Thread URL private val _shareThreadUrlResult = MutableSharedFlow() val shareThreadUrlResult = _shareThreadUrlResult.shareIn(viewModelScope, SharingStarted.Lazily) @@ -404,6 +416,11 @@ class MainViewModel @Inject constructor( refreshThreads(folderId = folderId) } + private fun openDraftFolder() { + val draftFolder = folderController.getFolder(FolderRole.DRAFT) + draftFolder?.let { folder -> openFolder(folder.id) } + } + fun flushFolder() = viewModelScope.launch(ioCoroutineContext) { val mailboxUuid = currentMailbox.value?.uuid ?: return@launch val folderId = currentFolderId ?: return@launch @@ -479,6 +496,7 @@ class MainViewModel @Inject constructor( deleteThreadsOrMessage(threadsUids = threadsUids) } + // TODO: When the back is done refactoring how scheduled drafts are deleted, work on this function shall resume. private fun deleteThreadsOrMessage( threadsUids: List, message: Message? = null, @@ -492,17 +510,21 @@ class MainViewModel @Inject constructor( val shouldPermanentlyDelete = isPermanentDeleteFolder(getActionFolderRole(threads, message)) val messages = getMessagesToDelete(threads, message) - val uids = messages.getUids() + + val messagesUids = messages.filter { it.isScheduledDraft.not() }.getUids() + val scheduledMessagesUuid = messages.filter { it.isScheduledDraft }.getUids() threadController.updateIsLocallyMovedOutStatus(threadsUids, hasBeenMovedOut = true) val apiResponses = if (shouldPermanentlyDelete) { - ApiRepository.deleteMessages(mailbox.uuid, uids) + ApiRepository.deleteMessages(mailbox.uuid, messagesUids) + // TODO: Delete scheduled messages separately, but avoid making too much call (waiting for API changes). } else { trashId = folderController.getFolder(FolderRole.TRASH)!!.id - ApiRepository.moveMessages(mailbox.uuid, uids, trashId).also { + ApiRepository.moveMessages(mailbox.uuid, messagesUids, trashId).also { undoResources = it.mapNotNull { apiResponse -> apiResponse.data?.undoResource } } + // TODO: Move? scheduled messages separately. } deleteThreadOrMessageTrigger.postValue(Unit) @@ -586,10 +608,71 @@ class MainViewModel @Inject constructor( showDraftDeletedSnackbar(apiResponse) } + fun deleteScheduleDraft(scheduleAction: String) = viewModelScope.launch(ioCoroutineContext) { + val mailbox = currentMailbox.value!! + val apiResponse = ApiRepository.deleteScheduleDraft(scheduleAction) + + if (apiResponse.isSuccess()) { + val draftFolderId = folderController.getFolder(FolderRole.SCHEDULED_DRAFTS)!!.id + refreshFoldersAsync(mailbox, listOf(draftFolderId)) + } + + showScheduleDraftDeletedSnackbar(apiResponse) + } + private fun showDraftDeletedSnackbar(apiResponse: ApiResponse) { val titleRes = if (apiResponse.isSuccess()) R.string.snackbarDraftDeleted else apiResponse.translateError() snackbarManager.postValue(appContext.getString(titleRes)) } + + private fun showScheduleDraftDeletedSnackbar(apiResponse: ApiResponse) { + if (apiResponse.isSuccess()) { + snackbarManager.postValue( + title = appContext.getString(R.string.snackbarSaveInDraft), + buttonTitle = R.string.draftFolder, + customBehavior = ::openDraftFolder, + ) + } else { + snackbarManager.postValue(appContext.getString(apiResponse.translateError())) + } + } + //endregion + + //region Schedule draft + private fun getScheduleDraft(draftResource: String, onSuccess: () -> Unit) = viewModelScope.launch(ioCoroutineContext) { + val apiResponse = ApiRepository.getDraft(draftResource) + + if (apiResponse.isSuccess()) { + onSuccess() + } else { + snackbarManager.postValue(title = appContext.getString(apiResponse.translatedError)) + } + } + + fun rescheduleDraft(draftResource: String, scheduleDate: Date) = viewModelScope.launch(ioCoroutineContext) { + val apiResponse = ApiRepository.rescheduleDraft(draftResource, scheduleDate) + + if (apiResponse.isSuccess()) { + refreshScheduleDraftFolder() + } else { + snackbarManager.postValue(title = appContext.getString(apiResponse.translatedError)) + } + } + + fun modifyDraft(scheduleAction: String, draftResource: String, onSuccess: () -> Unit) = + viewModelScope.launch(ioCoroutineContext) { + val mailbox = currentMailbox.value!! + val apiResponse = ApiRepository.deleteScheduleDraft(scheduleAction) + + if (apiResponse.isSuccess()) { + val draftFolderId = folderController.getFolder(FolderRole.SCHEDULED_DRAFTS)!!.id + refreshFoldersAsync(mailbox, listOf(draftFolderId)) + + getScheduleDraft(draftResource, onSuccess) + } else { + snackbarManager.postValue(title = appContext.getString(apiResponse.translatedError)) + } + } //endregion //region Move @@ -1136,13 +1219,13 @@ class MainViewModel @Inject constructor( selectedThreadsLiveData.value = selectedThreads } - fun refreshDraftFolderWhenDraftArrives(scheduledDate: Long) = viewModelScope.launch(ioCoroutineContext) { + fun refreshDraftFolderWhenDraftArrives(etopScheduledDate: Long) = viewModelScope.launch(ioCoroutineContext) { val folder = folderController.getFolder(FolderRole.DRAFT) if (folder?.cursor != null) { val timeNow = Date().time - val delay = REFRESH_DELAY + max(scheduledDate - timeNow, 0L) + val delay = REFRESH_DELAY + max(etopScheduledDate - timeNow, 0L) delay(min(delay, MAX_REFRESH_DELAY)) refreshController.refreshThreads( @@ -1154,6 +1237,19 @@ class MainViewModel @Inject constructor( } } + private fun refreshScheduleDraftFolder() = viewModelScope.launch(ioCoroutineContext) { + val folder = folderController.getFolder(FolderRole.SCHEDULED_DRAFTS) + + if (folder?.cursor != null) { + refreshController.refreshThreads( + refreshMode = RefreshMode.REFRESH_FOLDER_WITH_ROLE, + mailbox = currentMailbox.value!!, + folderId = folder.id, + realm = mailboxContentRealm(), + ) + } + } + fun handleDeletedMessages(messagesUids: Set) = viewModelScope.launch(ioCoroutineContext) { snackbarManager.postValue(appContext.getString(R.string.snackbarDeletedConversation)) diff --git a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/ConfirmScheduledDraftModificationDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/ConfirmScheduledDraftModificationDialog.kt new file mode 100644 index 0000000000..1713fd6980 --- /dev/null +++ b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/ConfirmScheduledDraftModificationDialog.kt @@ -0,0 +1,116 @@ +/* + * Infomaniak Mail - Android + * Copyright (C) 2024-2025 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.mail.ui.alertDialogs + +import android.content.Context +import androidx.annotation.StringRes +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.infomaniak.lib.core.utils.context +import com.infomaniak.mail.R +import com.infomaniak.mail.databinding.DialogConfirmScheduledDraftModificationBinding +import dagger.hilt.android.qualifiers.ActivityContext +import dagger.hilt.android.scopes.ActivityScoped +import javax.inject.Inject +import com.infomaniak.lib.core.R as RCore + +@ActivityScoped +open class ConfirmScheduledDraftModificationDialog @Inject constructor( + @ActivityContext private val activityContext: Context, +) : BaseAlertDialog(activityContext) { + + val binding: DialogConfirmScheduledDraftModificationBinding by lazy { + DialogConfirmScheduledDraftModificationBinding.inflate( + activity.layoutInflater + ) + } + + override val alertDialog = initDialog() + + private var onPositiveButtonClicked: (() -> Unit)? = null + private var onNegativeButtonClicked: (() -> Unit)? = null + private var onDismissed: (() -> Unit)? = null + + private fun initDialog(customThemeRes: Int? = null) = with(binding) { + + val builder = customThemeRes?.let { MaterialAlertDialogBuilder(context, it) } ?: MaterialAlertDialogBuilder(context) + + builder + .setView(root) + .setPositiveButton(R.string.buttonConfirm, null) + .setNegativeButton(RCore.string.buttonCancel, null) + .create() + } + + final override fun resetCallbacks() { + onPositiveButtonClicked = null + onNegativeButtonClicked = null + onDismissed = null + } + + fun show( + title: String, + description: String, + onPositiveButtonClicked: () -> Unit, + onNegativeButtonClicked: (() -> Unit)? = null, + onDismiss: (() -> Unit)? = null, + ) { + showDialogWithBasicInfo(title, description, R.string.buttonModify) + setupListeners(onPositiveButtonClicked, onNegativeButtonClicked, onDismiss) + } + + private fun setupListeners( + onPositiveButtonClicked: () -> Unit, + onNegativeButtonClicked: (() -> Unit)?, + onDismiss: (() -> Unit)?, + ) = with(alertDialog) { + + this@ConfirmScheduledDraftModificationDialog.onPositiveButtonClicked = onPositiveButtonClicked + this@ConfirmScheduledDraftModificationDialog.onNegativeButtonClicked = onNegativeButtonClicked + + positiveButton.setOnClickListener { + this@ConfirmScheduledDraftModificationDialog.onPositiveButtonClicked?.invoke() + dismiss() + } + + negativeButton.setOnClickListener { + this@ConfirmScheduledDraftModificationDialog.onNegativeButtonClicked?.invoke() + dismiss() + } + + onDismiss.let { + onDismissed = it + setOnDismissListener { onDismissed?.invoke() } + } + } + + private fun showDialogWithBasicInfo( + title: String? = null, + description: String? = null, + @StringRes positiveButtonText: Int? = null, + @StringRes negativeButtonText: Int? = null, + ) = with(binding) { + + alertDialog.show() + + title?.let(dialogDescriptionLayout.dialogTitle::setText) + description?.let(dialogDescriptionLayout.dialogDescription::setText) + + positiveButtonText?.let(positiveButton::setText) + negativeButtonText?.let(negativeButton::setText) + } +} diff --git a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt new file mode 100644 index 0000000000..75384c4d0f --- /dev/null +++ b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt @@ -0,0 +1,205 @@ +/* + * Infomaniak Mail - Android + * Copyright (C) 2024-2025 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.mail.ui.alertDialogs + +import android.content.Context +import androidx.annotation.StringRes +import androidx.core.view.isVisible +import com.google.android.material.datepicker.* +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.timepicker.MaterialTimePicker +import com.google.android.material.timepicker.TimeFormat +import com.infomaniak.lib.core.utils.* +import com.infomaniak.mail.R +import com.infomaniak.mail.data.LocalSettings +import com.infomaniak.mail.databinding.DialogSelectDateAndTimeForScheduledDraftBinding +import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialog.Companion.MAX_SCHEDULE_DELAY_YEARS +import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialog.Companion.MIN_SCHEDULE_DELAY_MINUTES +import dagger.hilt.android.qualifiers.ActivityContext +import dagger.hilt.android.scopes.ActivityScoped +import java.util.Date +import javax.inject.Inject +import com.infomaniak.lib.core.R as RCore + +@ActivityScoped +open class SelectDateAndTimeForScheduledDraftDialog @Inject constructor( + @ActivityContext private val activityContext: Context, +) : BaseAlertDialog(activityContext) { + + val binding: DialogSelectDateAndTimeForScheduledDraftBinding by lazy { + DialogSelectDateAndTimeForScheduledDraftBinding.inflate( + activity.layoutInflater + ) + } + + override val alertDialog = initDialog() + + private var onPositiveButtonClicked: (() -> Unit)? = null + private var onNegativeButtonClicked: (() -> Unit)? = null + private var onDismissed: (() -> Unit)? = null + + lateinit var selectedDate: Date + + private var datePicker: MaterialDatePicker? = null + private var timePicker: MaterialTimePicker? = null + + @Inject + lateinit var localSettings: LocalSettings + + private fun initDialog(customThemeRes: Int? = null) = with(binding) { + + val builder = customThemeRes?.let { MaterialAlertDialogBuilder(context, it) } ?: MaterialAlertDialogBuilder(context) + + selectedDate = Date().roundUpToNextTenMinutes() + + setTimePicker() + setDatePicker() + + dateField.setText(selectedDate.format(FORMAT_DATE_DAY_MONTH_YEAR)) + + builder + .setView(root) + .setPositiveButton(R.string.buttonConfirm, null) + .setNegativeButton(RCore.string.buttonCancel, null) + .create() + } + + final override fun resetCallbacks() { + onPositiveButtonClicked = null + onNegativeButtonClicked = null + onDismissed = null + } + + fun show( + title: String, + onPositiveButtonClicked: () -> Unit, + onNegativeButtonClicked: (() -> Unit)? = null, + onDismiss: (() -> Unit)? = null, + ) { + showDialogWithBasicInfo(title, R.string.buttonScheduleTitle) + setupListeners(onPositiveButtonClicked, onNegativeButtonClicked, onDismiss) + } + + private fun getScheduleDateErrorText(): String = if (selectedDate.isInTheFuture().not()) { + activityContext.resources.getString(R.string.errorChooseUpcomingDate) + } else { + activityContext.resources.getQuantityString( + R.plurals.errorScheduleDelayTooShort, + MIN_SCHEDULE_DELAY_MINUTES, + MIN_SCHEDULE_DELAY_MINUTES, + ) + } + + private fun setupListeners( + onPositiveButtonClicked: () -> Unit, + onNegativeButtonClicked: (() -> Unit)?, + onDismiss: (() -> Unit)?, + ) = with(alertDialog) { + + binding.dateField.setOnClickListener { datePicker?.show(super.activity.supportFragmentManager, "tag") } + + datePicker?.addOnPositiveButtonClickListener { time -> + val date = Date().also { it.time = time } + selectedDate = selectedDate.setDay(date.day()) + + binding.dateField.setText(selectedDate.format(FORMAT_DATE_DAY_MONTH_YEAR)) + } + + binding.timeField.setOnClickListener { timePicker?.show(super.activity.supportFragmentManager, "tag") } + + timePicker?.addOnPositiveButtonClickListener { + val hour: Int = timePicker!!.hour + val minute: Int = timePicker!!.minute + + selectedDate = selectedDate.setHour(hour) + selectedDate = selectedDate.setMinute(minute) + + binding.timeField.setText(selectedDate.format(FORMAT_DATE_HOUR_MINUTE)) + + binding.scheduleDateError.text = getScheduleDateErrorText() + binding.scheduleDateError.isVisible = selectedDate.isAtLeastXMinutesInTheFuture(MIN_SCHEDULE_DELAY_MINUTES).not() + positiveButton.isEnabled = selectedDate.isAtLeastXMinutesInTheFuture(MIN_SCHEDULE_DELAY_MINUTES) + } + + this@SelectDateAndTimeForScheduledDraftDialog.onPositiveButtonClicked = onPositiveButtonClicked + this@SelectDateAndTimeForScheduledDraftDialog.onNegativeButtonClicked = onNegativeButtonClicked + + positiveButton.setOnClickListener { + this@SelectDateAndTimeForScheduledDraftDialog.onPositiveButtonClicked?.invoke() + dismiss() + } + + negativeButton.setOnClickListener { + this@SelectDateAndTimeForScheduledDraftDialog.onNegativeButtonClicked?.invoke() + dismiss() + } + + onDismiss.let { + onDismissed = it + setOnDismissListener { onDismissed?.invoke() } + } + } + + private fun setTimePicker() = with(binding) { + timePicker = MaterialTimePicker.Builder() + .setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK) + .setTimeFormat(TimeFormat.CLOCK_24H) + .setHour(selectedDate.hours()) + .setMinute(selectedDate.minutes()) + .setTitleText(context.getString(R.string.selectTimeDialogTitle)) + .build() + } + + private fun setDatePicker() = with(binding) { + val dateValidators = listOf( + DateValidatorPointForward.now(), + DateValidatorPointBackward.before(Date().addYears(MAX_SCHEDULE_DELAY_YEARS).time), + ) + + val constraintsBuilder = + CalendarConstraints.Builder() + .setValidator(CompositeDateValidator.allOf(dateValidators)) + + datePicker = MaterialDatePicker.Builder.datePicker() + .setTitleText(context.getString(R.string.selectDateDialogTitle)) + .setSelection(MaterialDatePicker.todayInUtcMilliseconds()) + .setCalendarConstraints(constraintsBuilder.build()) + .build() + } + + private fun showDialogWithBasicInfo( + title: String? = null, + @StringRes positiveButtonText: Int? = null, + @StringRes negativeButtonText: Int? = null, + ) = with(binding) { + + alertDialog.show() + + selectedDate = Date().roundUpToNextTenMinutes() + + binding.timeField.setText(selectedDate.format(FORMAT_DATE_HOUR_MINUTE)) + + setTimePicker() + setDatePicker() + + title?.let(dialogTitle::setText) + + positiveButtonText?.let(positiveButton::setText) + negativeButtonText?.let(negativeButton::setText) + } +} diff --git a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt new file mode 100644 index 0000000000..6717354a5d --- /dev/null +++ b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt @@ -0,0 +1,224 @@ +/* + * Infomaniak Mail - Android + * Copyright (C) 2024-2025 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.mail.ui.bottomSheetDialogs + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.navArgs +import com.infomaniak.lib.core.utils.* +import com.infomaniak.mail.MatomoMail.trackScheduleSendEvent +import com.infomaniak.mail.R +import com.infomaniak.mail.data.LocalSettings +import com.infomaniak.mail.databinding.BottomSheetScheduleSendBinding +import com.infomaniak.mail.ui.MainViewModel +import com.infomaniak.mail.ui.main.thread.actions.ActionItemView +import com.infomaniak.mail.ui.main.thread.actions.ActionsBottomSheetDialog +import com.infomaniak.mail.ui.newMessage.NewMessageViewModel +import com.infomaniak.mail.utils.MailDateFormatUtils.mostDetailedDate +import dagger.hilt.android.AndroidEntryPoint +import java.util.Date +import javax.inject.Inject + + +@AndroidEntryPoint +class ScheduleSendBottomSheetDialog @Inject constructor() : ActionsBottomSheetDialog() { + + private val navigationArgs: ScheduleSendBottomSheetDialogArgs by navArgs() + + private var binding: BottomSheetScheduleSendBinding by safeBinding() + override val mainViewModel: MainViewModel by activityViewModels() + + private val newMessageViewModel: NewMessageViewModel by activityViewModels() + + @Inject + lateinit var localSettings: LocalSettings + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return BottomSheetScheduleSendBinding.inflate(inflater, container, false).also { binding = it }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?): Unit = with(binding) { + super.onViewCreated(view, savedInstanceState) + + localSettings.lastSelectedScheduleDate?.let { lastSelectedSchedule -> + if (Date(lastSelectedSchedule).isAtLeastXMinutesInTheFuture(MIN_SCHEDULE_DELAY_MINUTES)) { + lastScheduleItem.isVisible = true + lastScheduleItem.setDescription( + mostDetailedDate( + context, + date = Date(lastSelectedSchedule), + format = FORMAT_DATE_DAY_MONTH, + ) + ) + } + } + + lastScheduleItem.setClosingOnClickListener { + val draftResource = navigationArgs.draftResource + val lastSelectedScheduleDate = localSettings.lastSelectedScheduleDate + + if (navigationArgs.isAlreadyScheduled) { + if (draftResource != null && lastSelectedScheduleDate != null) { + trackScheduleSendEvent("lastSchedule") + mainViewModel.rescheduleDraft(draftResource, Date(lastSelectedScheduleDate)) + } + } else { + lastSelectedScheduleDate?.let { + trackScheduleSendEvent("lastSchedule") + newMessageViewModel.setScheduleDate(Date(it)) + newMessageViewModel.triggerSendMessage() + } + } + } + + if (newMessageViewModel.currentMailbox.isFree) { + customScheduleItem.setOnClickListener { + safeNavigate( + resId = R.id.upgradeProductBottomSheetDialog, + currentClassName = ScheduleSendBottomSheetDialog::class.java.name, + ) + } + } else { + customScheduleItem.setClosingOnClickListener { + if (navigationArgs.isAlreadyScheduled) { + mainViewModel.showSelectDateAndTimeForScheduleDialog() + } else { + newMessageViewModel.showSelectDateAndTimeForScheduleDialog() + } + } + } + + fun createScheduleItem(schedule: Schedule): ActionItemView = ActionItemView(this.context).apply { + setTitle(schedule.scheduleTitleRes) + setDescription(mostDetailedDate(context, date = schedule.date, format = FORMAT_DATE_DAY_MONTH)) + setIconResource(schedule.scheduleIconRes) + setClosingOnClickListener { + if (navigationArgs.isAlreadyScheduled) { + navigationArgs.draftResource?.let { + trackScheduleSendEvent(schedule.matomoValue) + mainViewModel.rescheduleDraft(draftResource = it, schedule.date) + } + } else { + trackScheduleSendEvent(schedule.matomoValue) + newMessageViewModel.setScheduleDate(schedule.date) + newMessageViewModel.triggerSendMessage() + } + } + } + + val timeToDisplay = TimeToDisplay.getTimeToDisplayFromDate() + Schedule.entries.filter { schedule -> timeToDisplay in schedule.timeToDisplay }.forEach { schedule -> + scheduleItems.addView(createScheduleItem(schedule)) + } + + val shouldDisplayDivider = lastScheduleItem.isVisible + (scheduleItems.children.first() as ActionItemView).setDividerVisibility(shouldDisplayDivider) + } + + companion object { + const val MIN_SCHEDULE_DELAY_MINUTES = 5 + const val MAX_SCHEDULE_DELAY_YEARS = 10 + } +} + +enum class TimeToDisplay { + NIGHT, + MORNING, + AFTERNOON, + EVENING, + WEEKEND; + + companion object { + fun getTimeToDisplayFromDate(): TimeToDisplay { + val now = Date() + val timeSlot = Date(now.time) + return if (now.isWeekend()) WEEKEND else when (now) { + in timeSlot.setHour(0).setMinute(0)..timeSlot.setHour(7).setMinute(54) -> NIGHT + in timeSlot.setHour(7).setMinute(55)..timeSlot.setHour(13).setMinute(54) -> MORNING + in timeSlot.setHour(13).setMinute(55)..timeSlot.setHour(17).setMinute(54) -> AFTERNOON + in timeSlot.setHour(17).setMinute(55)..timeSlot.setHour(23).setMinute(54) -> EVENING + else -> NIGHT + } + } + } +} + +enum class Schedule( + @StringRes val scheduleTitleRes: Int, + @DrawableRes val scheduleIconRes: Int, + val date: Date, + val timeToDisplay: List, + val matomoValue: String, +) { + LATER_THIS_MORNING( + R.string.laterThisMorning, + R.drawable.ic_morning_sunrise_schedule, + Date().getMorning(), + listOf(TimeToDisplay.NIGHT), + "laterThisMorning", + ), + MONDAY_MORNING( + R.string.mondayMorning, + R.drawable.ic_morning_schedule, + Date().getNextMonday().getMorning(), + listOf(TimeToDisplay.WEEKEND), + "nextMondayMorning", + ), + MONDAY_AFTERNOON( + R.string.mondayAfternoon, + R.drawable.ic_afternoon_schedule, + Date().getNextMonday().getAfternoon(), + listOf(TimeToDisplay.WEEKEND), + "nextMondayAfternoon", + ), + THIS_AFTERNOON( + R.string.thisAfternoon, + R.drawable.ic_afternoon_schedule, + Date().getAfternoon(), + listOf(TimeToDisplay.MORNING), + "thisAfternoon", + ), + THIS_EVENING( + R.string.thisEvening, + R.drawable.ic_evening_schedule, + Date().getEvening(), + listOf(TimeToDisplay.AFTERNOON), + "thisEvening", + ), + TOMORROW_MORNING( + R.string.tomorrowMorning, + R.drawable.ic_morning_schedule, + Date().getTomorrow().getMorning(), + listOf(TimeToDisplay.NIGHT, TimeToDisplay.MORNING, TimeToDisplay.AFTERNOON, TimeToDisplay.EVENING), + "tomorrowMorning", + ), + NEXT_MONDAY( + R.string.nextMonday, + R.drawable.ic_arrow_return, + Date().getNextMonday().getMorning(), + listOf(TimeToDisplay.NIGHT, TimeToDisplay.MORNING, TimeToDisplay.AFTERNOON, TimeToDisplay.EVENING), + "nextMonday", + ) +} diff --git a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/UpgradeProductBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/UpgradeProductBottomSheetDialog.kt new file mode 100644 index 0000000000..a8f14722e1 --- /dev/null +++ b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/UpgradeProductBottomSheetDialog.kt @@ -0,0 +1,41 @@ +/* + * Infomaniak Mail - Android + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.mail.ui.bottomSheetDialogs + +import android.os.Bundle +import android.view.View +import androidx.core.view.isGone +import com.infomaniak.mail.R + +class UpgradeProductBottomSheetDialog : InformationBottomSheetDialog() { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?): Unit = with(binding) { + super.onViewCreated(view, savedInstanceState) + + title.setText(R.string.disabledFeatureFlagTitle) + description.text = getString(R.string.disabledFeatureFlagDescription) + infoIllustration.setBackgroundResource(R.drawable.ic_update_logo) + + actionButton.apply { + actionButton.setText(R.string.buttonClose) + setOnClickListener { dismiss() } + } + + secondaryActionButton.isGone = true + } +} diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt index 1639309de9..6bcba6ee1e 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -231,6 +231,7 @@ class ThreadListAdapter @Inject constructor( mailBodyPreview.text = computePreview().ifBlank { context.getString(R.string.noBodyTitle) } mailDate.text = formatDate(context) + scheduleSendIcon.isVisible = numberOfScheduledDrafts > 0 && folderRole == FolderRole.SCHEDULED_DRAFTS draftPrefix.isVisible = hasDrafts val (isIconReplyVisible, isIconForwardVisible) = computeReplyAndForwardIcon(thread.isAnswered, thread.isForwarded) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt index c840b87b46..65868576b8 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt @@ -86,15 +86,17 @@ class TwoPaneViewModel @Inject constructor( messageUid: String? = null, mailToUri: Uri? = null, ) { - newMessageArgs.value = NewMessageActivityArgs( - draftMode = draftMode, - previousMessageUid = previousMessageUid, - shouldLoadDistantResources = shouldLoadDistantResources, - arrivedFromExistingDraft = arrivedFromExistingDraft, - draftLocalUuid = draftLocalUuid, - draftResource = draftResource, - messageUid = messageUid, - mailToUri = mailToUri, + newMessageArgs.postValue( + NewMessageActivityArgs( + draftMode = draftMode, + previousMessageUid = previousMessageUid, + shouldLoadDistantResources = shouldLoadDistantResources, + arrivedFromExistingDraft = arrivedFromExistingDraft, + draftLocalUuid = draftLocalUuid, + draftResource = draftResource, + messageUid = messageUid, + mailToUri = mailToUri, + ) ) } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/FolderViewHolder.kt b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/FolderViewHolder.kt index 4dcaf3b69a..6d0431438e 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/FolderViewHolder.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/menuDrawer/items/FolderViewHolder.kt @@ -66,7 +66,7 @@ class FolderViewHolder( } val unread = when (folder.role) { - FolderRole.DRAFT -> UnreadDisplay(count = folder.threads.count()) + FolderRole.DRAFT, FolderRole.SCHEDULED_DRAFTS -> UnreadDisplay(count = folder.threads.count()) FolderRole.SENT, FolderRole.TRASH -> UnreadDisplay(count = 0) else -> folder.unreadCountDisplay } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/MessageAlertView.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/MessageAlertView.kt index b1e6c01546..be768f948a 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/MessageAlertView.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/MessageAlertView.kt @@ -51,6 +51,10 @@ class MessageAlertView @JvmOverloads constructor( } } + fun setDescription(text: String) = with(binding) { + description.text = text + } + fun onAction1(listener: OnClickListener) { binding.action1.setOnClickListener(listener) } 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 d6fd979184..530787fd12 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 @@ -39,8 +39,10 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.viewbinding.ViewBinding +import com.infomaniak.lib.core.utils.FORMAT_DATE_DAY_FULL_MONTH_WITH_TIME import com.infomaniak.lib.core.utils.FormatterFileSize.formatShortFileSize import com.infomaniak.lib.core.utils.context +import com.infomaniak.lib.core.utils.format import com.infomaniak.lib.core.utils.isNightModeEnabled import com.infomaniak.mail.MatomoMail.trackMessageEvent import com.infomaniak.mail.R @@ -201,7 +203,7 @@ class ThreadAdapter( initMapForNewMessage(message, position) bindHeader(message) - bindAlerts(message.uid) + bindAlerts(message) bindCalendarEvent(message) bindAttachments(message) bindContent(message) @@ -350,6 +352,19 @@ class ThreadAdapter( private fun MessageViewHolder.bindHeader(message: Message) = with(binding) { val messageDate = message.date.toDate() + if (message.isScheduledDraft) { + scheduleSendIcon.isVisible = true + + scheduleAlert.setDescription( + context.getString( + R.string.scheduledEmailHeader, + message.date.toDate().format(FORMAT_DATE_DAY_FULL_MONTH_WITH_TIME), + ) + ) + alertsGroup.isVisible = true + scheduleAlert.isVisible = true + } + if (message.isDraft) { userAvatar.loadAvatar(AccountUtils.currentUser!!) expeditorName.apply { @@ -437,12 +452,18 @@ class ThreadAdapter( detailedMessageDate.text = mostDetailedDate(context, messageDate) } - private fun MessageViewHolder.bindAlerts(messageUid: String) = with(binding) { + private fun MessageViewHolder.bindAlerts(message: Message) = with(binding) { + message.draftResource?.let { draftResource -> + scheduleAlert.onAction1 { threadAdapterCallbacks?.onRescheduleClicked?.invoke(draftResource) } + } + + scheduleAlert.onAction2 { threadAdapterCallbacks?.onModifyClicked?.invoke(message) } + distantImagesAlert.onAction1 { bodyWebViewClient.unblockDistantResources() fullMessageWebViewClient.unblockDistantResources() - manuallyAllowedMessagesUids.add(messageUid) + manuallyAllowedMessagesUids.add(message.uid) reloadVisibleWebView() @@ -745,6 +766,8 @@ class ThreadAdapter( var navigateToDownloadProgressDialog: ((Attachment, AttachmentIntentType) -> Unit)? = null, var replyToCalendarEvent: ((AttendanceState, Message) -> Unit)? = null, var promptLink: ((String, ContextMenuType) -> Unit)? = null, + var onRescheduleClicked: ((String) -> Unit)? = null, + var onModifyClicked: ((Message) -> Unit)? = null, ) private enum class DisplayType(val layout: Int) { diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt index 0004b68e17..982461b66a 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -36,6 +36,7 @@ import androidx.lifecycle.distinctUntilChanged import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy import com.infomaniak.lib.core.utils.SentryLog import com.infomaniak.lib.core.utils.context +import com.infomaniak.lib.core.utils.safeNavigate import com.infomaniak.lib.core.views.DividerItemDecorator import com.infomaniak.mail.MatomoMail.ACTION_ARCHIVE_NAME import com.infomaniak.mail.MatomoMail.ACTION_DELETE_NAME @@ -65,6 +66,7 @@ import com.infomaniak.mail.data.models.thread.Thread import com.infomaniak.mail.databinding.FragmentThreadBinding import com.infomaniak.mail.ui.MainViewModel import com.infomaniak.mail.ui.alertDialogs.* +import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialogArgs import com.infomaniak.mail.ui.main.SnackbarManager import com.infomaniak.mail.ui.main.folder.TwoPaneFragment import com.infomaniak.mail.ui.main.folder.TwoPaneViewModel @@ -82,6 +84,7 @@ import com.infomaniak.mail.utils.extensions.AttachmentExtensions.openAttachment import dagger.hilt.android.AndroidEntryPoint import io.sentry.Sentry import io.sentry.SentryLevel +import java.util.Date import javax.inject.Inject import kotlin.math.absoluteValue import kotlin.math.min @@ -121,6 +124,12 @@ class ThreadFragment : Fragment() { @Inject lateinit var snackbarManager: SnackbarManager + @Inject + lateinit var selectDateAndTimeForScheduledDraftDialog: SelectDateAndTimeForScheduledDraftDialog + + @Inject + lateinit var confirmScheduledDraftModificationDialog: ConfirmScheduledDraftModificationDialog + private var _binding: FragmentThreadBinding? = null private val binding get() = _binding!! // This property is only valid between onCreateView and onDestroyView @@ -159,6 +168,8 @@ class ThreadFragment : Fragment() { observeReportDisplayProblemResult() observeMessageOfUserToBlock() + + observeSelectDateAndTimeForScheduleDialogState() } private fun observeReportDisplayProblemResult() { @@ -177,6 +188,28 @@ class ThreadFragment : Fragment() { } } + private fun observeSelectDateAndTimeForScheduleDialogState() { + mainViewModel.showOrCloseSelectDateAndTimeForScheduleDialog.observe(viewLifecycleOwner) { + selectDateAndTimeForScheduledDraftDialog.show( + title = getString(R.string.datePickerTitle), + onPositiveButtonClicked = { + val scheduleDate = selectDateAndTimeForScheduledDraftDialog.selectedDate.time + localSettings.lastSelectedScheduleDate = scheduleDate + + mainViewModel.draftResource?.let { draftResource -> + mainViewModel.rescheduleDraft(draftResource, Date(scheduleDate)) + } + }, + onNegativeButtonClicked = { + safeNavigate( + resId = R.id.scheduleSendBottomSheetDialog, + currentClassName = ThreadFragment::class.java.name, + ) + }, + ) + } + } + override fun onConfigurationChanged(newConfig: Configuration) { threadAdapter.reRenderMails() super.onConfigurationChanged(newConfig) @@ -328,6 +361,38 @@ class ThreadFragment : Fragment() { ContextMenuType.PHONE -> phoneContextualMenuAlertDialog.show(data) } }, + onRescheduleClicked = { draftResource -> + mainViewModel.draftResource = draftResource + safeNavigate( + resId = R.id.scheduleSendBottomSheetDialog, + args = ScheduleSendBottomSheetDialogArgs( + isAlreadyScheduled = true, + draftResource = draftResource, + ).toBundle(), + currentClassName = ThreadFragment::class.java.name, + ) + }, + onModifyClicked = { message -> + confirmScheduledDraftModificationDialog.show( + title = getString(R.string.editSendTitle), + description = getString(R.string.editSendDescription), + onPositiveButtonClicked = { + val scheduleAction = message.scheduleAction + val draftResource = message.draftResource + + if (scheduleAction != null && draftResource != null) { + mainViewModel.modifyDraft(scheduleAction, draftResource) { + trackNewMessageEvent(OPEN_FROM_DRAFT_NAME) + twoPaneViewModel.navigateToNewMessage( + arrivedFromExistingDraft = true, + draftResource = message.draftResource, + messageUid = message.uid, + ) + } + } + }, + ) + } ), ) @@ -415,6 +480,8 @@ class ThreadFragment : Fragment() { } iconTint = ColorStateList.valueOf(color) } + + binding.quickActionBar.isGone = thread.numberOfScheduledDrafts == thread.messages.size } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ActionItemView.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ActionItemView.kt index 9989950a76..7b98ebbe31 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ActionItemView.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ActionItemView.kt @@ -23,15 +23,15 @@ import android.content.res.TypedArray import android.util.AttributeSet import android.view.LayoutInflater import android.widget.FrameLayout -import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.annotation.StyleableRes import androidx.appcompat.content.res.AppCompatResources +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isGone import androidx.core.view.isVisible import com.infomaniak.lib.core.utils.getAttributes -import com.infomaniak.lib.core.utils.setPaddingRelative +import com.infomaniak.lib.core.utils.setMarginsRelative import com.infomaniak.mail.R import com.infomaniak.mail.databinding.ItemBottomSheetActionBinding import com.infomaniak.mail.utils.AccountUtils @@ -47,20 +47,21 @@ class ActionItemView @JvmOverloads constructor( init { attrs?.getAttributes(context, R.styleable.ActionItemView) { with(binding) { - button.apply { - icon = getDrawable(R.styleable.ActionItemView_icon) - getColorStateList(R.styleable.ActionItemView_iconColor)?.let(::setIconTint) - text = getString(R.styleable.ActionItemView_text) - getColorStateList(R.styleable.ActionItemView_textColor)?.let(::setTextColor) - - val iconHorizontalPadding = getDimenOrNull(R.styleable.ActionItemView_iconPaddingHorizontal) - val iconPaddingStart = iconHorizontalPadding ?: getDimenOrNull(R.styleable.ActionItemView_iconPaddingStart) - val iconPaddingEnd = iconHorizontalPadding ?: getDimenOrNull(R.styleable.ActionItemView_iconPaddingEnd) - - iconPaddingEnd?.let { iconPadding = it } - setPaddingRelative(start = iconPaddingStart) + icon.setImageDrawable(getDrawable(R.styleable.ActionItemView_icon)) + getColorStateList(R.styleable.ActionItemView_iconColor)?.let(::setIconTint) ?: run { + icon.imageTintList = AppCompatResources.getColorStateList(context, R.color.icon_button_primary_color) } + title.text = getString(R.styleable.ActionItemView_title) + getColorStateList(R.styleable.ActionItemView_titleColor)?.let(::setTitleColor) + + val iconHorizontalPadding = getDimenOrNull(R.styleable.ActionItemView_iconPaddingHorizontal) + val iconPaddingStart = iconHorizontalPadding ?: getDimenOrNull(R.styleable.ActionItemView_iconPaddingStart) + val iconPaddingEnd = iconHorizontalPadding ?: getDimenOrNull(R.styleable.ActionItemView_iconPaddingEnd) + + icon.setMarginsRelative(start = iconPaddingStart) + description.setMarginsRelative(end = iconPaddingEnd) + divider.apply { isVisible = getBoolean(R.styleable.ActionItemView_visibleDivider, true) dividerColor = getColor(R.styleable.ActionItemView_dividerColor, context.getColor(R.color.dividerColor)) @@ -68,31 +69,40 @@ class ActionItemView @JvmOverloads constructor( if (getBoolean(R.styleable.ActionItemView_staffOnly, false)) { if (isInEditMode || AccountUtils.currentUser?.isStaff == true) { - button.apply { - setIconTintResource(R.color.staffOnlyColor) - setTextColor(AppCompatResources.getColorStateList(context, R.color.staffOnlyColor)) - } + icon.imageTintList = AppCompatResources.getColorStateList(context, R.color.staffOnlyColor) + title.setTextColor(AppCompatResources.getColorStateList(context, R.color.staffOnlyColor)) } else { isGone = true } } - if (getBoolean(R.styleable.ActionItemView_keepIconTint, false)) button.iconTint = null + if (getBoolean(R.styleable.ActionItemView_keepIconTint, false)) icon.imageTintList = null + + description.isVisible = true + + if (getBoolean(R.styleable.ActionItemView_showActionIcon, false)) actionIcon.isVisible = true } } } override fun setOnClickListener(onClickListener: OnClickListener?) { - binding.button.setOnClickListener(onClickListener) + findViewById(R.id.itemBottomSheetAction).setOnClickListener(onClickListener) } - fun setIconResource(@DrawableRes iconResourceId: Int) = binding.button.setIconResource(iconResourceId) + fun setIconResource(@DrawableRes iconResourceId: Int) = binding.icon.setImageResource(iconResourceId) - fun setIconTint(@ColorInt color: Int) { - binding.button.iconTint = ColorStateList.valueOf(color) + private fun setIconTint(color: ColorStateList) { + binding.icon.imageTintList = color } - fun setText(@StringRes textResourceId: Int) = binding.button.setText(textResourceId) + fun setTitle(@StringRes textResourceId: Int) = binding.title.setText(textResourceId) + + private fun setTitleColor(color: ColorStateList) = binding.title.setTextColor(color) + + fun setDescription(text: String) = with(binding.description) { + this.text = text + isVisible = true + } fun setDividerVisibility(isVisible: Boolean) { binding.divider.isVisible = isVisible diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MailActionsBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MailActionsBottomSheetDialog.kt index de26b453eb..b7570be3ff 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MailActionsBottomSheetDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MailActionsBottomSheetDialog.kt @@ -112,19 +112,19 @@ abstract class MailActionsBottomSheetDialog : ActionsBottomSheetDialog() { fun setMarkAsReadUi(isSeen: Boolean) = with(binding.markAsReadUnread) { val (readIconRes, readTextRes) = computeUnreadStyle(isSeen) setIconResource(readIconRes) - setText(readTextRes) + setTitle(readTextRes) } fun setFavoriteUi(isFavorite: Boolean) = with(binding.favorite) { val (favoriteIconRes, favoriteText) = computeFavoriteStyle(isFavorite) setIconResource(favoriteIconRes) - setText(favoriteText) + setTitle(favoriteText) } fun setArchiveUi(isFromArchive: Boolean) = with(binding.archive) { if (isFromArchive) { setIconResource(R.drawable.ic_drawer_inbox) - setText(R.string.actionMoveToInbox) + setTitle(R.string.actionMoveToInbox) } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MessageActionsBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MessageActionsBottomSheetDialog.kt index 2d416ef7a6..0cccbc3418 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MessageActionsBottomSheetDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MessageActionsBottomSheetDialog.kt @@ -74,7 +74,7 @@ class MessageActionsBottomSheetDialog : MailActionsBottomSheetDialog() { if (requireContext().isNightModeEnabled()) { binding.lightTheme.apply { isVisible = true - setText(if (isThemeTheSame) R.string.actionViewInLight else R.string.actionViewInDark) + setTitle(if (isThemeTheSame) R.string.actionViewInLight else R.string.actionViewInDark) setClosingOnClickListener { mainViewModel.toggleLightThemeForMessage.value = message } } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MultiSelectBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MultiSelectBottomSheetDialog.kt index 3ccdd2f2dc..516d89cc30 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MultiSelectBottomSheetDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MultiSelectBottomSheetDialog.kt @@ -131,14 +131,14 @@ class MultiSelectBottomSheetDialog : ActionsBottomSheetDialog() { val (spamIcon, spamText) = getSpamIconAndText(isFromSpam) binding.spam.apply { setIconResource(spamIcon) - setText(spamText) + setTitle(spamText) } val favoriteIcon = if (shouldFavorite) R.drawable.ic_star else R.drawable.ic_unstar val favoriteText = if (shouldFavorite) R.string.actionStar else R.string.actionUnstar binding.favorite.apply { setIconResource(favoriteIcon) - setText(favoriteText) + setTitle(favoriteText) } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ThreadActionsBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ThreadActionsBottomSheetDialog.kt index 30c2560abd..bf2b0b6005 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ThreadActionsBottomSheetDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ThreadActionsBottomSheetDialog.kt @@ -96,7 +96,7 @@ class ThreadActionsBottomSheetDialog : MailActionsBottomSheetDialog() { R.string.actionReportJunk to R.drawable.ic_report_junk } - setText(text) + setTitle(text) setIconResource(icon) isVisible = true } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt index 5293f93816..2f5d3fda1a 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt @@ -124,8 +124,16 @@ class NewMessageActivity : BaseActivity() { } private fun saveDraft() { + val draftAction = if (newMessageViewModel.shouldScheduleInsteadOfSend) { + DraftAction.SCHEDULE + } else if (newMessageViewModel.shouldSendInsteadOfSave) { + DraftAction.SEND + } else { + DraftAction.SAVE + } + val draftSaveConfiguration = DraftSaveConfiguration( - action = if (newMessageViewModel.shouldSendInsteadOfSave) DraftAction.SEND else DraftAction.SAVE, + action = draftAction, isFinishing = isFinishing, isTaskRoot = isTaskRoot, startWorkerCallback = ::startWorker, @@ -135,7 +143,10 @@ class NewMessageActivity : BaseActivity() { } private fun startWorker() { - draftsActionsWorkerScheduler.scheduleWork(newMessageViewModel.draftLocalUuid()) + draftsActionsWorkerScheduler.scheduleWork( + draftLocalUuid = newMessageViewModel.draftLocalUuid(), + scheduleDate = newMessageViewModel.scheduleDate, + ) } data class DraftSaveConfiguration( diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageEditorManager.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageEditorManager.kt index 1f64e879ac..1163158bfe 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageEditorManager.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageEditorManager.kt @@ -126,7 +126,7 @@ class NewMessageEditorManager @Inject constructor(private val insertLinkDialog: } editorActions.isGone = isExpanded - sendButton.isGone = isExpanded + sendLayout.isGone = isExpanded formatOptionsScrollView.isVisible = isExpanded } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 9c27a642ee..a3da783766 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -43,11 +43,8 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController -import com.infomaniak.lib.core.utils.FilePicker +import com.infomaniak.lib.core.utils.* import com.infomaniak.lib.core.utils.SnackbarUtils.showSnackbar -import com.infomaniak.lib.core.utils.getBackNavigationResult -import com.infomaniak.lib.core.utils.isNightModeEnabled -import com.infomaniak.lib.core.utils.showToast import com.infomaniak.lib.richhtmleditor.StatusCommand.* import com.infomaniak.mail.MatomoMail.OPEN_FROM_DRAFT_NAME import com.infomaniak.mail.MatomoMail.trackAttachmentActionsEvent @@ -57,6 +54,7 @@ import com.infomaniak.mail.data.LocalSettings import com.infomaniak.mail.data.LocalSettings.ExternalContent import com.infomaniak.mail.data.models.Attachment import com.infomaniak.mail.data.models.AttachmentDisposition +import com.infomaniak.mail.data.models.FeatureFlag import com.infomaniak.mail.data.models.draft.Draft import com.infomaniak.mail.data.models.draft.Draft.DraftAction import com.infomaniak.mail.data.models.draft.Draft.DraftMode @@ -65,6 +63,7 @@ import com.infomaniak.mail.databinding.FragmentNewMessageBinding import com.infomaniak.mail.ui.MainActivity import com.infomaniak.mail.ui.alertDialogs.DescriptionAlertDialog import com.infomaniak.mail.ui.alertDialogs.InformationAlertDialog +import com.infomaniak.mail.ui.alertDialogs.SelectDateAndTimeForScheduledDraftDialog import com.infomaniak.mail.ui.main.SnackbarManager import com.infomaniak.mail.ui.main.thread.AttachmentAdapter import com.infomaniak.mail.ui.newMessage.NewMessageRecipientFieldsManager.FieldType @@ -89,6 +88,7 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import java.util.Date import javax.inject.Inject import com.google.android.material.R as RMaterial @@ -150,6 +150,9 @@ class NewMessageFragment : Fragment() { @Inject lateinit var snackbarManager: SnackbarManager + @Inject + lateinit var selectDateAndTimeForScheduledDraftDialog: SelectDateAndTimeForScheduledDraftDialog + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return FragmentNewMessageBinding.inflate(inflater, container, false).also { _binding = it }.root } @@ -204,6 +207,35 @@ class NewMessageFragment : Fragment() { observeContacts() observeCcAndBccVisibility() } + + if (newMessageViewModel.currentMailbox.featureFlags.contains(FeatureFlag.SCHEDULE_SEND_DRAFT)) { + binding.scheduleSendButton.isVisible = true + } + + newMessageViewModel.sendMessageTrigger.observe(viewLifecycleOwner) { tryToSendEmail(scheduled = true) } + + observeSelectDateAndTimeForScheduleDialogState() + } + + private fun observeSelectDateAndTimeForScheduleDialogState() { + newMessageViewModel.showOrCloseSelectDateAndTimeForScheduleDialog.observe(viewLifecycleOwner) { showDialog -> + if (showDialog) { + selectDateAndTimeForScheduledDraftDialog.show( + title = getString(R.string.datePickerTitle), + onPositiveButtonClicked = { + val scheduleDate = selectDateAndTimeForScheduledDraftDialog.selectedDate.time + localSettings.lastSelectedScheduleDate = scheduleDate + + newMessageViewModel.setScheduleDate(Date(scheduleDate)) + + tryToSendEmail(scheduled = true) + }, + onNegativeButtonClicked = { safeNavigate(resId = R.id.scheduleSendBottomSheetDialog) }, + ) + } else { + selectDateAndTimeForScheduledDraftDialog.resetLoadingAndDismiss() + } + } } private fun observeShimmering() { @@ -339,7 +371,7 @@ class NewMessageFragment : Fragment() { initEditorUi() - setupSendButton() + setupSendButtons() externalsManager.setupExternalBanner() scrim.setOnClickListener { @@ -680,19 +712,24 @@ class NewMessageFragment : Fragment() { newMessageViewModel.deleteAttachment(position) } - private fun setupSendButton() = with(binding) { + private fun setupSendButtons() = with(binding) { newMessageViewModel.isSendingAllowed.observe(viewLifecycleOwner) { + scheduleSendButton.isEnabled = it sendButton.isEnabled = it } + scheduleSendButton.setOnClickListener { safeNavigate(resId = R.id.scheduleSendBottomSheetDialog) } sendButton.setOnClickListener { tryToSendEmail() } } - private fun tryToSendEmail() { + private fun tryToSendEmail(scheduled: Boolean = false) { fun setSnackbarActivityResult() { val resultIntent = Intent() - resultIntent.putExtra(MainActivity.DRAFT_ACTION_KEY, DraftAction.SEND.name) + resultIntent.putExtra( + MainActivity.DRAFT_ACTION_KEY, + if (scheduled) DraftAction.SCHEDULE.name else DraftAction.SEND.name, + ) requireActivity().setResult(AppCompatActivity.RESULT_OK, resultIntent) } 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 7b77b0a769..3442b153f8 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 @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -81,6 +81,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import org.jsoup.nodes.Document +import java.util.Date import javax.inject.Inject @HiltViewModel @@ -122,6 +123,10 @@ class NewMessageViewModel @Inject constructor( val editorBodyInitializer = SingleLiveEvent() + val showOrCloseSelectDateAndTimeForScheduleDialog = SingleLiveEvent() + + fun showSelectDateAndTimeForScheduleDialog() = showOrCloseSelectDateAndTimeForScheduleDialog.postValue(true) + // 1. Navigating to AiPropositionFragment causes NewMessageFragment to export its body to `subjectAndBodyChannel`. // 2. Inserting the AI proposition navigates back to NewMessageFragment. // 3. When saving or sending the draft now, the channel still holds the previous body as it wasn't consumed. @@ -134,9 +139,11 @@ class NewMessageViewModel @Inject constructor( var isAutoCompletionOpened = false var isEditorExpanded = false var isExternalBannerManuallyClosed = false + var shouldScheduleInsteadOfSend = false var shouldSendInsteadOfSave = false var signaturesCount = 0 private var isNewMessage = false + var scheduleDate: Date? = null private var snapshot: DraftSnapshot? = null @@ -152,6 +159,7 @@ class NewMessageViewModel @Inject constructor( val editorAction = SingleLiveEvent>() // Needs to trigger every time the Fragment is recreated val initResult = MutableLiveData() + val sendMessageTrigger = SingleLiveEvent() private val _isShimmering = MutableStateFlow(true) val isShimmering: StateFlow = _isShimmering @@ -193,6 +201,7 @@ class NewMessageViewModel @Inject constructor( fun draftLocalUuid() = draftLocalUuid fun draftMode() = draftMode fun shouldLoadDistantResources() = shouldLoadDistantResources + fun triggerSendMessage() = sendMessageTrigger.postValue(true) fun initDraftAndViewModel(intent: Intent): LiveData = liveData(ioCoroutineContext) { @@ -855,6 +864,19 @@ class NewMessageViewModel @Inject constructor( }.onFailure(Sentry::captureException) } + fun setScheduleDate(scheduleDate: Date) = viewModelScope.launch(ioDispatcher) { + val localUuid = draftLocalUuid ?: return@launch + this@NewMessageViewModel.scheduleDate = scheduleDate + + shouldScheduleInsteadOfSend = true + + mailboxContentRealm().write { + DraftController.getDraft(localUuid, realm = this)?.also { draft -> + draft.scheduleDate = this@NewMessageViewModel.scheduleDate?.format(FORMAT_SCHEDULE_MAIL) + } + } + } + fun storeBodyAndSubject(subject: String, html: String) { globalCoroutineScope.launch(ioDispatcher) { _subjectAndBodyChannel.send(SubjectAndBodyData(subject, html, channelExpirationIdTarget)) @@ -1036,6 +1058,9 @@ class NewMessageViewModel @Inject constructor( DraftAction.SEND -> { if (isTaskRoot) appContext.showToast(R.string.snackbarEmailSending) } + DraftAction.SCHEDULE -> { + if (isTaskRoot) appContext.showToast(R.string.snackbarScheduling) + } } } diff --git a/app/src/main/java/com/infomaniak/mail/utils/MailDateFormatUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/MailDateFormatUtils.kt index 8687a89a05..c55fa8f2ba 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/MailDateFormatUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/MailDateFormatUtils.kt @@ -47,10 +47,10 @@ object MailDateFormatUtils { } } - fun mostDetailedDate(context: Context, date: Date): String = with(date) { + fun mostDetailedDate(context: Context, date: Date, format: String = FORMAT_EMAIL_DATE_LONG_DATE): String = with(date) { return@with context.getString( R.string.messageDetailsDateAt, - format(FORMAT_EMAIL_DATE_LONG_DATE), + format(format), format(FORMAT_EMAIL_DATE_HOUR), ) } diff --git a/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt b/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt index bfd5dc9d54..17782b3b9a 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -121,6 +121,8 @@ import java.util.Date import java.util.Scanner import kotlin.math.roundToInt +const val IK_FOLDER = ".ik" + //region Type alias // Explanation of this Map: Map> typealias MergedContactDictionary = Map> @@ -338,17 +340,26 @@ fun List.flattenFolderChildrenAndRemoveMessages(dismissHiddenChildren: B val folder = inputList.removeAt(0) + /* + * There are two types of folders: + * - user's folders (with or without a role) + * - hidden IK folders (scheduled drafts, snoozed, etc…) + * + * We want to display the user's folders, and also the IK folders for which we handle the role. + * IK folders where we don't handle the role are dismissed. + */ + fun shouldThisFolderBeAdded(): Boolean = folder.path.startsWith(IK_FOLDER).not() || folder.role != null + val children = if (folder.isManaged()) { - outputList.add(folder.copyFromRealm(depth = 1u)) + if (shouldThisFolderBeAdded()) outputList.add(folder.copyFromRealm(depth = 1u)) + with(folder.children) { - (if (dismissHiddenChildren) query("${Folder::isHidden.name} == false") else query()) - .sortFolders() - .find() + (if (dismissHiddenChildren) query("${Folder::isHidden.name} == false") else query()).sortFolders().find() } } else { - outputList.add(folder) - (if (dismissHiddenChildren) folder.children.filter { !it.isHidden } else folder.children) - .sortFolders() + if (shouldThisFolderBeAdded()) outputList.add(folder) + + (if (dismissHiddenChildren) folder.children.filter { !it.isHidden } else folder.children).sortFolders() } inputList.addAll(index = 0, children) @@ -576,7 +587,7 @@ fun Context.postfixWithTag( tag, getTagsPaint(this), ellipsizeConfiguration.maxWidth, - ellipsizeConfiguration.truncateAt + ellipsizeConfiguration.truncateAt, ).toString() } ?: tag } diff --git a/app/src/main/java/com/infomaniak/mail/workers/BaseCoroutineWorker.kt b/app/src/main/java/com/infomaniak/mail/workers/BaseCoroutineWorker.kt index c9d0b8ad94..f4ae94377f 100644 --- a/app/src/main/java/com/infomaniak/mail/workers/BaseCoroutineWorker.kt +++ b/app/src/main/java/com/infomaniak/mail/workers/BaseCoroutineWorker.kt @@ -54,5 +54,7 @@ abstract class BaseCoroutineWorker(appContext: Context, params: WorkerParameters companion object { private const val MAX_RETRIES = 3 + + fun Data.getLongOrNull(key: String) = getLong(key, 0).run { if (this == 0L) null else this } } } 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 9a216f8590..bfcbf8a2ca 100644 --- a/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt +++ b/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt @@ -32,9 +32,13 @@ import com.infomaniak.mail.R import com.infomaniak.mail.data.api.ApiRepository import com.infomaniak.mail.data.cache.RealmDatabase import com.infomaniak.mail.data.cache.mailboxContent.DraftController +import com.infomaniak.mail.data.cache.mailboxContent.FolderController +import com.infomaniak.mail.data.cache.mailboxContent.RefreshController +import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RefreshMode import com.infomaniak.mail.data.cache.mailboxInfo.MailboxController import com.infomaniak.mail.data.models.AppSettings import com.infomaniak.mail.data.models.AttachmentUploadStatus +import com.infomaniak.mail.data.models.Folder.FolderRole import com.infomaniak.mail.data.models.draft.Draft import com.infomaniak.mail.data.models.draft.Draft.DraftAction import com.infomaniak.mail.data.models.mailbox.Mailbox @@ -70,6 +74,8 @@ class DraftsActionsWorker @AssistedInject constructor( private val mainApplication: MainApplication, private val notificationManagerCompat: NotificationManagerCompat, private val notificationUtils: NotificationUtils, + private val folderController: FolderController, + private val refreshController: RefreshController, @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : BaseCoroutineWorker(appContext, params) { @@ -83,6 +89,9 @@ class DraftsActionsWorker @AssistedInject constructor( private lateinit var userApiToken: String private var isSnackbarFeedbackNeeded: Boolean = false + private var scheduleDate: Long? = null + private var scheduleAction: String? = null + private val dateFormatWithTimezone by lazy { SimpleDateFormat(FORMAT_DATE_WITH_TIMEZONE, Locale.ROOT) } override suspend fun launchWork(): Result = withContext(ioDispatcher) { @@ -95,6 +104,9 @@ class DraftsActionsWorker @AssistedInject constructor( mailboxId = inputData.getIntOrNull(MAILBOX_ID_KEY) ?: return@withContext Result.failure() draftLocalUuid = inputData.getString(DRAFT_LOCAL_UUID_KEY) + scheduleDate = inputData.getLongOrNull(SCHEDULE_DATE_KEY) ?: return@withContext Result.failure() + scheduleAction = inputData.getString(SCHEDULE_ACTION_KEY) + userApiToken = AccountUtils.getUserById(userId)?.apiToken?.accessToken ?: return@withContext Result.failure() mailbox = mailboxController.getMailbox(userId, mailboxId) ?: return@withContext Result.failure() okHttpClient = AccountUtils.getHttpClient(userId) @@ -124,7 +136,7 @@ class DraftsActionsWorker @AssistedInject constructor( // List containing the callback function to update/delete drafts in Realm // We keep these Realm changes in a List to execute them all in a unique Realm transaction at the end of the worker val realmActionsOnDraft = mutableListOf<(MutableRealm) -> Unit>() - val scheduledDates = mutableListOf() + val etopScheduledDates = mutableListOf() var trackedDraftErrorMessageResId: Int? = null var remoteUuidOfTrackedDraft: String? = null var trackedDraftAction: DraftAction? = null @@ -137,6 +149,7 @@ class DraftsActionsWorker @AssistedInject constructor( var haveAllDraftsSucceeded = true drafts.asReversed().forEach { draft -> + val isTargetDraft = draft.localUuid == draftLocalUuid if (isTargetDraft) trackedDraftAction = draft.action @@ -148,8 +161,10 @@ class DraftsActionsWorker @AssistedInject constructor( remoteUuidOfTrackedDraft = savedDraftUuid isTrackedDraftSuccess = true } - scheduledDate?.let(scheduledDates::add) + etopScheduledDate?.let(etopScheduledDates::add) realmActionOnDraft?.let(realmActionsOnDraft::add) + + this@DraftsActionsWorker.scheduleAction = scheduleAction } else if (isTargetDraft) { trackedDraftErrorMessageResId = errorMessageResId!! isTrackedDraftSuccess = false @@ -203,7 +218,7 @@ class DraftsActionsWorker @AssistedInject constructor( showDraftErrorNotification(isTrackedDraftSuccess, trackedDraftErrorMessageResId, trackedDraftAction) return computeResult( - scheduledDates, + etopScheduledDates, haveAllDraftsSucceeded, isTrackedDraftSuccess, remoteUuidOfTrackedDraft, @@ -231,8 +246,22 @@ class DraftsActionsWorker @AssistedInject constructor( } } + private suspend fun refreshScheduleDraftFolder() { + val currentMailbox = mailboxController.getMailbox(AccountUtils.currentUserId, AccountUtils.currentMailboxId) ?: return + val folder = folderController.getFolder(FolderRole.SCHEDULED_DRAFTS) + + if (folder?.cursor != null) { + refreshController.refreshThreads( + refreshMode = RefreshMode.REFRESH_FOLDER_WITH_ROLE, + mailbox = currentMailbox, + folderId = folder.id, + realm = mailboxContentRealm, + ) + } + } + private fun computeResult( - scheduledDates: MutableList, + etopScheduledDates: MutableList, haveAllDraftsSucceeded: Boolean, isTrackedDraftSuccess: Boolean?, remoteUuidOfTrackedDraft: String?, @@ -240,7 +269,7 @@ class DraftsActionsWorker @AssistedInject constructor( trackedDraftErrorMessageResId: Int?, ): Result { - val biggestScheduledDate = scheduledDates.mapNotNull { dateFormatWithTimezone.parse(it)?.time }.maxOrNull() + val biggestEtopScheduledDate = etopScheduledDates.mapNotNull { dateFormatWithTimezone.parse(it)?.time }.maxOrNull() return if (haveAllDraftsSucceeded || isTrackedDraftSuccess == true) { val outputData = if (isSnackbarFeedbackNeeded) { @@ -248,8 +277,10 @@ class DraftsActionsWorker @AssistedInject constructor( REMOTE_DRAFT_UUID_KEY to draftLocalUuid?.let { remoteUuidOfTrackedDraft }, ASSOCIATED_MAILBOX_UUID_KEY to draftLocalUuid?.let { mailbox.uuid }, RESULT_DRAFT_ACTION_KEY to draftLocalUuid?.let { trackedDraftAction?.name }, - BIGGEST_SCHEDULED_DATE_KEY to biggestScheduledDate, + BIGGEST_ETOP_SCHEDULED_DATE_KEY to biggestEtopScheduledDate, RESULT_USER_ID_KEY to userId, + SCHEDULE_DATE_KEY to scheduleDate, + SCHEDULE_ACTION_KEY to scheduleAction, ) } else { Data.EMPTY @@ -259,7 +290,7 @@ class DraftsActionsWorker @AssistedInject constructor( val outputData = if (isSnackbarFeedbackNeeded) { workDataOf( ERROR_MESSAGE_RESID_KEY to trackedDraftErrorMessageResId, - BIGGEST_SCHEDULED_DATE_KEY to biggestScheduledDate, + BIGGEST_ETOP_SCHEDULED_DATE_KEY to biggestEtopScheduledDate, RESULT_USER_ID_KEY to userId, ) } else { @@ -271,7 +302,8 @@ class DraftsActionsWorker @AssistedInject constructor( data class DraftActionResult( val realmActionOnDraft: ((MutableRealm) -> Unit)?, - val scheduledDate: String?, + val etopScheduledDate: String?, + val scheduleAction: String?, val errorMessageResId: Int?, val savedDraftUuid: String?, val isSuccess: Boolean, @@ -280,7 +312,8 @@ class DraftsActionsWorker @AssistedInject constructor( private suspend fun executeDraftAction(draft: Draft, mailboxUuid: String, isFirstTime: Boolean = true): DraftActionResult { var realmActionOnDraft: ((MutableRealm) -> Unit)? = null - var scheduledDate: String? = null + var etopScheduledDate: String? = null + var scheduleAction: String? = null var savedDraftUuid: String? = null SentryDebug.addDraftBreadcrumbs(draft, step = "executeDraftAction (action = ${draft.action?.name.toString()})") @@ -299,7 +332,8 @@ class DraftsActionsWorker @AssistedInject constructor( return DraftActionResult( realmActionOnDraft = null, - scheduledDate = null, + etopScheduledDate = null, + scheduleAction = null, errorMessageResId = R.string.errorCorruptAttachment, savedDraftUuid = null, isSuccess = false, @@ -315,7 +349,7 @@ class DraftsActionsWorker @AssistedInject constructor( action = null } } - scheduledDate = dateFormatWithTimezone.format(Date()) + etopScheduledDate = dateFormatWithTimezone.format(Date()) savedDraftUuid = data.draftRemoteUuid } ?: run { retryWithNewIdentityOrThrow(draft, mailboxUuid, isFirstTime) @@ -325,7 +359,7 @@ class DraftsActionsWorker @AssistedInject constructor( suspend fun executeSendAction() = with(ApiRepository.sendDraft(mailboxUuid, draft, okHttpClient)) { when { isSuccess() -> { - scheduledDate = data?.scheduledDate + etopScheduledDate = data?.etopScheduledDate realmActionOnDraft = deleteDraftCallback(draft) } error?.exception is SerializationException -> { @@ -342,15 +376,38 @@ class DraftsActionsWorker @AssistedInject constructor( } } + suspend fun executeScheduleSendAction() = with(ApiRepository.sendScheduleDraft(mailboxUuid, draft, okHttpClient)) { + when { + isSuccess() -> { + scheduleAction = data?.scheduleAction + realmActionOnDraft = deleteDraftCallback(draft) + refreshScheduleDraftFolder() + } + error?.exception is SerializationException -> { + realmActionOnDraft = deleteDraftCallback(draft) + Sentry.captureMessage("Return JSON for SendScheduleDraft API call was modified", SentryLevel.ERROR) { scope -> + scope.setExtra("Is data null ?", "${data == null}") + scope.setExtra("Error code", error?.code.toString()) + scope.setExtra("Error description", error?.description.toString()) + } + } + else -> { + retryWithNewIdentityOrThrow(draft, mailboxUuid, isFirstTime) + } + } + } + when (draft.action) { DraftAction.SAVE -> executeSaveAction() DraftAction.SEND -> executeSendAction() + DraftAction.SCHEDULE -> executeScheduleSendAction() else -> Unit } return DraftActionResult( realmActionOnDraft = realmActionOnDraft, - scheduledDate = scheduledDate, + etopScheduledDate = etopScheduledDate, + scheduleAction = scheduleAction, errorMessageResId = null, savedDraftUuid = savedDraftUuid, isSuccess = true, @@ -398,7 +455,7 @@ class DraftsActionsWorker @AssistedInject constructor( private val workManager: WorkManager, ) { - fun scheduleWork(draftLocalUuid: String? = null) { + fun scheduleWork(draftLocalUuid: String? = null, scheduleDate: Date? = null) { if (AccountUtils.currentMailboxId == AppSettings.DEFAULT_ID) return if (draftController.getDraftsWithActionsCount() == 0L) return @@ -409,6 +466,7 @@ class DraftsActionsWorker @AssistedInject constructor( USER_ID_KEY to AccountUtils.currentUserId, MAILBOX_ID_KEY to AccountUtils.currentMailboxId, DRAFT_LOCAL_UUID_KEY to draftLocalUuid, + SCHEDULE_DATE_KEY to scheduleDate?.time, ) val workRequest = OneTimeWorkRequestBuilder() .addTag(TAG) @@ -442,7 +500,9 @@ class DraftsActionsWorker @AssistedInject constructor( const val REMOTE_DRAFT_UUID_KEY = "remoteDraftUuidKey" const val ASSOCIATED_MAILBOX_UUID_KEY = "associatedMailboxUuidKey" const val RESULT_DRAFT_ACTION_KEY = "resultDraftActionKey" - const val BIGGEST_SCHEDULED_DATE_KEY = "biggestScheduledDateKey" + const val BIGGEST_ETOP_SCHEDULED_DATE_KEY = "biggestEtopScheduledDateKey" const val RESULT_USER_ID_KEY = "resultUserIdKey" + const val SCHEDULE_DATE_KEY = "scheduleDateKey" + const val SCHEDULE_ACTION_KEY = "scheduleActionKey" } } diff --git a/app/src/main/res/drawable/ic_afternoon_schedule.xml b/app/src/main/res/drawable/ic_afternoon_schedule.xml new file mode 100644 index 0000000000..ee7943e1da --- /dev/null +++ b/app/src/main/res/drawable/ic_afternoon_schedule.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_return.xml b/app/src/main/res/drawable/ic_arrow_return.xml new file mode 100644 index 0000000000..478ddd1fa9 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_return.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_evening_schedule.xml b/app/src/main/res/drawable/ic_evening_schedule.xml new file mode 100644 index 0000000000..c32c397d15 --- /dev/null +++ b/app/src/main/res/drawable/ic_evening_schedule.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_last_schedule_selected.xml b/app/src/main/res/drawable/ic_last_schedule_selected.xml new file mode 100644 index 0000000000..b931125c20 --- /dev/null +++ b/app/src/main/res/drawable/ic_last_schedule_selected.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_morning_schedule.xml b/app/src/main/res/drawable/ic_morning_schedule.xml new file mode 100644 index 0000000000..22d3ca9a10 --- /dev/null +++ b/app/src/main/res/drawable/ic_morning_schedule.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_morning_sunrise_schedule.xml b/app/src/main/res/drawable/ic_morning_sunrise_schedule.xml new file mode 100644 index 0000000000..acfcd05ba3 --- /dev/null +++ b/app/src/main/res/drawable/ic_morning_sunrise_schedule.xml @@ -0,0 +1,55 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_pen.xml b/app/src/main/res/drawable/ic_pen.xml new file mode 100644 index 0000000000..fbd0224698 --- /dev/null +++ b/app/src/main/res/drawable/ic_pen.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_schedule_send.xml b/app/src/main/res/drawable/ic_schedule_send.xml new file mode 100644 index 0000000000..f547892998 --- /dev/null +++ b/app/src/main/res/drawable/ic_schedule_send.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/layout/bottom_sheet_account.xml b/app/src/main/res/layout/bottom_sheet_account.xml index 26a28510f3..e6ba53cb4c 100644 --- a/app/src/main/res/layout/bottom_sheet_account.xml +++ b/app/src/main/res/layout/bottom_sheet_account.xml @@ -50,7 +50,7 @@ app:icon="@drawable/ic_add_thin" app:iconPaddingEnd="@dimen/marginStandardMedium" app:iconPaddingStart="@dimen/marginStandard" - app:text="@string/buttonAddAccount" + app:title="@string/buttonAddAccount" app:visibleDivider="false" /> diff --git a/app/src/main/res/layout/bottom_sheet_actions_menu.xml b/app/src/main/res/layout/bottom_sheet_actions_menu.xml index 305cab8a35..c0451e5cb7 100644 --- a/app/src/main/res/layout/bottom_sheet_actions_menu.xml +++ b/app/src/main/res/layout/bottom_sheet_actions_menu.xml @@ -43,7 +43,7 @@ android:layout_height="wrap_content" android:visibility="gone" app:icon="@drawable/ic_view_in_light" - app:text="@string/actionViewInLight" + app:title="@string/actionViewInLight" app:visibleDivider="false" tools:visibility="visible" /> @@ -54,7 +54,7 @@ android:layout_height="wrap_content" android:visibility="gone" app:icon="@drawable/ic_alarm_clock" - app:text="@string/actionPostpone" + app:title="@string/actionPostpone" app:visibleDivider="false" tools:visibility="visible" /> @@ -63,7 +63,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:icon="@drawable/ic_email_action_move" - app:text="@string/actionMove" + app:title="@string/actionMove" app:visibleDivider="false" /> @@ -81,7 +81,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:icon="@drawable/ic_envelope" - app:text="@string/actionMarkAsUnread" + app:title="@string/actionMarkAsUnread" app:visibleDivider="false" /> @@ -115,7 +115,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:icon="@drawable/ic_email_action_share" - app:text="@string/shareEmail" + app:title="@string/shareEmail" app:visibleDivider="false" /> diff --git a/app/src/main/res/layout/bottom_sheet_attachment_actions.xml b/app/src/main/res/layout/bottom_sheet_attachment_actions.xml index b99e1bc74d..cd672c9fc1 100644 --- a/app/src/main/res/layout/bottom_sheet_attachment_actions.xml +++ b/app/src/main/res/layout/bottom_sheet_attachment_actions.xml @@ -35,7 +35,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:icon="@drawable/ic_open_with" - app:text="@string/openWith" + app:title="@string/openWith" app:visibleDivider="false" /> + app:title="@string/saveToDriveItem" /> + app:title="@string/saveToDriveDeviceItem" /> diff --git a/app/src/main/res/layout/bottom_sheet_detailed_contact.xml b/app/src/main/res/layout/bottom_sheet_detailed_contact.xml index 40e34788f5..868e263737 100644 --- a/app/src/main/res/layout/bottom_sheet_detailed_contact.xml +++ b/app/src/main/res/layout/bottom_sheet_detailed_contact.xml @@ -58,7 +58,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:icon="@drawable/ic_contact_action_write" - app:text="@string/contactActionWriteEmail" + app:title="@string/contactActionWriteEmail" app:visibleDivider="false" /> + app:title="@string/contactActionAddToContacts" /> + app:title="@string/contactActionCopyEmailAddress" /> diff --git a/app/src/main/res/layout/bottom_sheet_junk.xml b/app/src/main/res/layout/bottom_sheet_junk.xml index cc02ae68aa..17611cb177 100644 --- a/app/src/main/res/layout/bottom_sheet_junk.xml +++ b/app/src/main/res/layout/bottom_sheet_junk.xml @@ -27,7 +27,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:icon="@drawable/ic_spam" - app:text="@string/actionSpam" + app:title="@string/actionSpam" app:visibleDivider="false" /> + app:title="@string/actionPhishing" /> + app:title="@string/actionBlockSender" /> diff --git a/app/src/main/res/layout/bottom_sheet_multi_select.xml b/app/src/main/res/layout/bottom_sheet_multi_select.xml index 2f5cca1586..32657584f8 100644 --- a/app/src/main/res/layout/bottom_sheet_multi_select.xml +++ b/app/src/main/res/layout/bottom_sheet_multi_select.xml @@ -35,14 +35,14 @@ android:layout_height="wrap_content" android:visibility="gone" app:icon="@drawable/ic_alarm_clock" - app:text="@string/actionPostpone" /> + app:title="@string/actionPostpone" /> + app:title="@string/actionStar" /> diff --git a/app/src/main/res/layout/bottom_sheet_schedule_send.xml b/app/src/main/res/layout/bottom_sheet_schedule_send.xml new file mode 100644 index 0000000000..105d9fc416 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_schedule_send.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/bottom_sheet_upgrade_product.xml b/app/src/main/res/layout/bottom_sheet_upgrade_product.xml new file mode 100644 index 0000000000..e5b2f2a724 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_upgrade_product.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/cardview_thread_item.xml b/app/src/main/res/layout/cardview_thread_item.xml index 2ef584493c..f93c127ea3 100644 --- a/app/src/main/res/layout/cardview_thread_item.xml +++ b/app/src/main/res/layout/cardview_thread_item.xml @@ -199,7 +199,7 @@ app:cardBackgroundColor="@color/backgroundColor" app:cardCornerRadius="2dp" app:layout_constraintBottom_toBottomOf="@id/expeditor" - app:layout_constraintEnd_toStartOf="@id/mailDate" + app:layout_constraintEnd_toStartOf="@id/scheduleSendIcon" app:layout_constraintStart_toEndOf="@id/expeditor" app:layout_constraintTop_toTopOf="@id/expeditor" app:strokeColor="@color/progressbarTrackColor" @@ -216,6 +216,20 @@ + + + + + + + diff --git a/app/src/main/res/layout/dialog_select_date_and_time_for_scheduled_draft.xml b/app/src/main/res/layout/dialog_select_date_and_time_for_scheduled_draft.xml new file mode 100644 index 0000000000..3c6fb671a6 --- /dev/null +++ b/app/src/main/res/layout/dialog_select_date_and_time_for_scheduled_draft.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_account_management_settings.xml b/app/src/main/res/layout/fragment_account_management_settings.xml index ae928ab1ac..ea7e8c7597 100644 --- a/app/src/main/res/layout/fragment_account_management_settings.xml +++ b/app/src/main/res/layout/fragment_account_management_settings.xml @@ -76,8 +76,8 @@ app:icon="@drawable/ic_bin" app:iconColor="@color/redDestructiveAction" app:iconSize="@dimen/standardIconSize" - app:text="@string/buttonAccountDelete" - app:textColor="@color/redDestructiveAction" /> + app:title="@string/buttonAccountDelete" + app:titleColor="@color/redDestructiveAction" /> @@ -666,17 +665,34 @@ - + app:layout_constraintTop_toTopOf="parent"> + + + + + + diff --git a/app/src/main/res/layout/item_bottom_sheet_action.xml b/app/src/main/res/layout/item_bottom_sheet_action.xml index 380e6d62b5..cc352847a8 100644 --- a/app/src/main/res/layout/item_bottom_sheet_action.xml +++ b/app/src/main/res/layout/item_bottom_sheet_action.xml @@ -18,28 +18,73 @@ + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true"> - + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + diff --git a/app/src/main/res/layout/item_message.xml b/app/src/main/res/layout/item_message.xml index 1184dfcf74..defca7bc0d 100644 --- a/app/src/main/res/layout/item_message.xml +++ b/app/src/main/res/layout/item_message.xml @@ -67,6 +67,7 @@ app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toEndOf="@id/userAvatar" app:layout_constraintTop_toTopOf="parent" + app:layout_goneMarginEnd="@dimen/marginStandardVerySmall" tools:text="@tools:sample/full_names" /> + + @@ -364,7 +380,18 @@ android:showDividers="middle" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/alertsTopDivider"> + app:layout_constraintTop_toBottomOf="@id/messageDetails"> + + + app:title="Copier l'adresse mail" /> + app:title="Supprimer" /> diff --git a/app/src/main/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml index 89b4387fcd..ef43f6c14e 100644 --- a/app/src/main/res/navigation/main_navigation.xml +++ b/app/src/main/res/navigation/main_navigation.xml @@ -246,6 +246,22 @@ app:argType="boolean" /> + + + + + + + diff --git a/app/src/main/res/navigation/new_message_navigation.xml b/app/src/main/res/navigation/new_message_navigation.xml index dfbcb652f4..8e75e159d6 100644 --- a/app/src/main/res/navigation/new_message_navigation.xml +++ b/app/src/main/res/navigation/new_message_navigation.xml @@ -80,4 +80,26 @@ app:argType="com.infomaniak.mail.utils.extensions.AttachmentExtensions$AttachmentIntentType" /> + + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index de034f547c..206aea7d5a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -133,6 +133,7 @@ Erstellen Ein Konto erstellen Einen Ordner hinzufügen + Benutzerdefinierter Zeitplan adresse abtrennen Herunterladen Alle herunterladen @@ -147,6 +148,7 @@ Mit einem anderen Konto anmelden Anmeldung Vielleicht + Ändern Mehr Mehr Speicherplatz erhalten Neue Nachricht @@ -154,8 +156,10 @@ In meinem Kalender öffnen Abfälle Passwort anfordern + Neu planen E-Mails wiederherstellen Planen Sie den Versand von E-Mails für einen späteren Zeitpunkt + Zeitplan Siehe Alle Teilen Sie @@ -185,6 +189,7 @@ Ich Kontakte Ausgewähltes Konto + Aktionssymbol Zurück %s löschen Entwurf löschen @@ -215,6 +220,9 @@ Antwort Speicherstatus Mail-Aktionen öffnen + Zeitplan-Symbol + Geplante Nachricht + Geplantes Datum Ausgewählte Dieser Ordner enthält ungelesene Nachrichten Benutzer-Avatar @@ -231,12 +239,17 @@ Name des Ordners Keine Übergeordneter Ordner + Wählen Sie ein Datum und eine Uhrzeit Datum: Die Löschung Ihres Kontos ist endgültig. Sie werden nicht in der Lage sein, Ihr Konto zu reaktivieren. + In Kürze können Sie die Beschränkungen Ihrer ik.me-E-Mail-Adresse entsperren\n(Speicherlimit, benutzerdefinierter Zeitplan, Senden und mehr ...) + Benötigen Sie mehr Speicherplatz und Funktionen? Um Anzeigeprobleme zu beheben, aktualisieren Sie bitte die Anwendung Android System Webview. Problem mit der Anzeige Ihrer E-Mails Entwürfe (Entwurf) + Programmierung abgebrochen. Diese Nachricht wird in Ihre Entwürfe verschoben, damit Sie sie senden können, wann immer Sie möchten. + Senden bearbeiten Sie sind dabei, eine Nachricht ohne Betreff zu senden. Möchten Sie fortfahren? Leeres Thema Hier gibt es nichts zu sehen. @@ -258,6 +271,7 @@ Es ist mindestens ein Empfänger erforderlich Dateianhang unauffindbar Beim Senden Ihrer Antwort ist ein Fehler aufgetreten + Wählen Sie ein zukünftiges Datum Der Entwurf konnte nicht bearbeitet werden, ein Anhang ist beschädigt Entwurf unauffindbar Geplante oder versendete Nachrichten können nicht geändert werden @@ -279,6 +293,10 @@ Der Ordnername sollte nicht länger als 255 Zeichen sein. Der Server lehnt alle Empfänger ab Der Server lehnt den Absender ab + + Sie können E-Mails frühestens in %d Minute senden + Wählen Sie ein zukünftiges Datum, das mindestens %d Minuten in der Zukunft liegt + Versandlimit erreicht Zu viele Empfänger Ordner kann nicht erstellt werden @@ -307,6 +325,8 @@ Von: Google Play Services sind erforderlich Posteingang + Zuletzt ausgewählter Zeitplan + Später heute Morgen und Laden… Der Zugriff auf Ihre E-Mail-Adresse ist derzeit gesperrt.\nFür weitere Informationen, FAQ lesen. @@ -327,6 +347,8 @@ Entwurf Am %1$s, %2$s schrieb: Anzeigen der Konversation + Montagnachmittag + Montagmorgen Ihre Mailbox ist voll und Sie können keine weiteren Nachrichten empfangen 🥲 Sie werden bald in der Lage sein, die Grenzen Ihrer ik.me-E-Mail-Adresse freizugeben (Speicherlimit, Senden, Weiterleiten, …) Ist Ihre Box voll? Holen Sie sich mehr Stauraum! @@ -348,6 +370,7 @@ %d neue Nachrichten Geben Sie Ihre Nachricht ein + Nächsten Montag Diese Nachricht ist leer. Keine Unterhaltung in %s ausgewählt Sie haben noch keinen Ordner… @@ -395,6 +418,8 @@ Mögen Sie Infomaniak Mail? Auf dem Gerät speichern In kDrive speichern + Planen Sie den Versand + Diese E-Mail wird an diesem Datum gesendet: %s Geplante Nachrichten Alle Meldungen Eine E-Mail suchen @@ -403,7 +428,9 @@ Lesen Ungelesen Suche + Datum auswählen Keine Unterschrift + Zeit auswählen Senden Sie Gesendete Nachrichten Akzentfarbe @@ -517,6 +544,9 @@ Nummer in die Zwischenablage kopiert Erfolgreich gemeldet Starten der Wiederherstellung + Ihre Nachricht wurde als Entwurf gespeichert. + Die Nachricht wird am %s verschickt + Die Nachricht wird geplant Absender erfolgreich auf die schwarze Liste gesetzt Erfolgreich auf die schwarze Liste gesetzte Absender @@ -542,6 +572,8 @@ Um die kSync-App aus dem Play Store herunterladen zu können, klicken Sie auf \"Installieren\" und kehren Sie dann zu Ihrer Mail-App zurück. Installierte Aktivieren Sie die Kalender und Adressbücher in kSync, die Sie mit Ihrem Telefon synchronisieren möchten. + Heute Nachmittag + Heute Abend Sind Sie sicher, dass Sie diese Nachricht endgültig löschen wollen? Sind Sie sicher, dass Sie diese Nachrichten endgültig löschen wollen? @@ -571,6 +603,7 @@ Meine Konten An: + Morgen früh Sie können diese Adresse nicht hinzufügen, da Sie die Höchstzahl an Empfängern erreicht haben Papierkorb (unbekannt) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index a7c7e29a87..1dbe7c5638 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -133,6 +133,7 @@ Cree Crear una cuenta Añadir una carpeta + Horario personalizado separar dirección Descargar Descargar todo @@ -147,6 +148,7 @@ Conectarse con otra cuenta Inicio de sesión Quizás + Modificar Más Más espacio de almacenamiento Nuevo mensaje @@ -154,8 +156,10 @@ Abrir en mi calendario Desechos Solicitar contraseña + Reprogramar Restaurar correos electrónicos Programar un correo electrónico para enviarlo más tarde + Cronograma Véase Todos Compartir @@ -185,6 +189,7 @@ Yo Contactos Cuenta seleccionada + Icono de acción Volver Borrar %s Borrar borrador @@ -215,6 +220,9 @@ Respuesta Estado de almacenamiento Acciones de mensajes abiertos + Icono de programa + Mensaje programado + Fecha programada Selección Esta carpeta contiene mensajes no leídos Avatar de usuario @@ -231,12 +239,17 @@ Nombre de la carpeta Ninguno Carpeta principal + Elige una fecha y hora Date: La eliminación de su cuenta será definitiva. No podrá reactivar su cuenta. + Pronto podrás desbloquear los límites de tu dirección de correo electrónico ik.me\n(límite de almacenamiento, programación personalizada, envío y más...) + ¿Necesita más almacenamiento y funciones? Para solucionar los problemas de visualización, actualice la aplicación Android System Webview. Problema de visualización de sus emails Borradores (Borrador) + Programmazione annullata. Questo messaggio verrà spostato nelle tue bozze per essere inviato quando vuoi. + Editar envío Está a punto de enviar un mensaje sin asunto. ¿Desea continuar? Asunto vacío Aquí no hay nada que ver. @@ -258,6 +271,7 @@ Se precisa al menos un destinatario No se ha encontrado el adjunto Se ha producido un error al enviar su respuesta + Elige una fecha futura No se ha podido gestionar el borrador, un archivo adjunto está dañado No se puede encontrar el borrador Imposible modificar un mensaje programado o enviado @@ -279,6 +293,10 @@ El nombre de la carpeta no debe superar los 255 caracteres El servidor rechaza todos los destinatarios El servidor rechaza el remitente + + No puedes programar un correo electrónico para dentro de menos de %d minuto + No puedes programar un correo electrónico para dentro de menos de %d minutos + Límite de envío alcanzado Demasiados destinatarios No se puede crear la carpeta @@ -307,6 +325,8 @@ De: Se requieren los servicios de Google Play Bandeja de entrada + Último horario seleccionado + Más tarde esta mañana y Cargando… El acceso a su dirección de correo electrónico está bloqueado.\nPara más información, lea las preguntas frecuentes. @@ -327,6 +347,8 @@ Borrador El %1$s, %2$s escribió: Mostrar la conversación + lunes por la tarde + Lunes mañana Tu buzón está lleno y no puedes recibir más mensajes 🥲 Pronto podrás desbloquear los límites de tu dirección de correo electrónico ik.me (límite de almacenamiento, envío, reenvío, …) ¿Está llena su caja? ¡Consiga más espacio de almacenamiento! @@ -348,6 +370,7 @@ %d nuevos mensajes Escriba su mensaje + El lunes próximo Este mensaje está vacío. Ninguna conversación seleccionada en %s Aún no tienes ninguna carpeta… @@ -380,6 +403,7 @@ Sin selección ¿Estás seguro de que quieres separar la dirección %s? Separar dirección + Este espacio pertenece a %s y centraliza los archivos comunes de tu organización. Lea las preguntas frecuentes Seguir leyendo Búsquedas recientes @@ -395,6 +419,8 @@ ¿Te gusta Infomaniak Mail? Guardar en el dispositivo Guardar en kDrive + Programar envío + Este correo electrónico se enviará en esta fecha: %s Mensajes programados Todos los mensajes Buscar un correo electrónico @@ -403,7 +429,9 @@ Leer No leídos Buscar en + Seleccione la fecha Sin firma + Selezionare l\'ora Enviar Mensajes enviados Color de acento @@ -517,6 +545,9 @@ Número copiado en el portapapeles Informado con éxito Iniciar la restauración + Su mensaje se ha guardado en borradores. + El mensaje será enviado el %s + El mensaje está siendo programado Remitente incluido en la lista negra Remitentes incluidos con éxito en la lista negra @@ -542,6 +573,8 @@ Para descargar la aplicación kSync desde Play Store, pulsa en \"Instalar\" y vuelve a la aplicación Correo. Instalada Active en kSync los calendarios y libretas de direcciones que desea sincronizar con su teléfono. + Esta tarde + Esta noche ¿Estás seguro de que quieres borrar este mensaje permanentemente? ¿Estás seguro de que quieres borrar estos mensajes permanentemente? @@ -571,6 +604,7 @@ Mis cuentas Para: + Mañana por la mañana No puede añadir esta dirección porque ha alcanzado el límite de destinatarios Basura (desconocido) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0063450084..4cefc15cd5 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -135,6 +135,7 @@ Créer Créer un compte Ajouter un dossier + Horaire personnalisé détacher l’adresse Télécharger Tout télécharger @@ -149,6 +150,7 @@ Me connecter avec un autre compte Se connecter Peut-être + Modifier Plus Obtenir plus de stockage Nouveau message @@ -156,8 +158,10 @@ Ouvrir dans mon calendrier Refuser Demander le mot de passe + Reprogrammer Restaurer des e-mails Programmer l’envoi de l’e-mail + Programmer Consulter Tout Partager @@ -187,6 +191,7 @@ Moi Contacts Compte sélectionné + Icône d\'action Retour Supprimer %s Supprimer le brouillon @@ -217,6 +222,9 @@ Réponse État du stockage Ouvrir les actions du message + Icône de programmation + Message programmé + Date programmée Sélectionné Ce dossier contient des messages non lus Avatar de l’utilisateur @@ -233,12 +241,17 @@ Nom du dossier Aucun Dossier parent + Choisir une date et une heure Date : La suppression de votre compte sera définitive. Vous ne pourrez pas réactiver votre compte. + Vous pourrez prochainement débloquer les limites de votre adresse mail ik.me \n(limite de stockage, horaire personnalisé, envoi et plus encore...) + Besoin de plus de stockage et de fonctionnalités ? Pour corriger les problèmes d’affichage, veuillez mettre à jour l’application Android System Webview. Problème d’affichage de vos e-mails Brouillons (Brouillon) + Programmation annulée. Ce message sera déplacé dans vos brouillons pour être envoyé quand vous le souhaitez. + Modifier l’envoi Vous êtes sur le point d’envoyer un message sans objet. Voulez-vous continuer ? Objet vide Il n’y a rien à voir par ici. @@ -260,6 +273,7 @@ Il faut au minimum un destinataire Pièce jointe introuvable Une erreur s’est produite lors de l’envoi de votre réponse + Choisissez une date à venir Échec du traitement du brouillon, une pièce jointe est corrompue Brouillon introuvable Impossible de modifier un message planifié ou envoyé @@ -281,6 +295,11 @@ Le nom du dossier ne doit pas dépasser 255 caractères Le serveur refuse tous les destinataires Le serveur refuse l’expéditeur + + Vous ne pouvez pas programmer un e-mail dans moins de %d minute + Vous ne pouvez pas programmer un e-mail dans moins de %d minutes + Vous ne pouvez pas programmer un e-mail dans moins de %d de minutes + Limite d’envoi atteinte Trop de destinataires Impossible de créer le dossier @@ -312,6 +331,8 @@ De : Les Google Play Services sont requis Boîte de réception + Dernier horaire choisi + Plus tard ce matin et Chargement… L’accès à votre adresse mail est actuellement bloqué.\nPour plus d’informations, consultez les FAQ. @@ -333,6 +354,8 @@ Brouillon Le %1$s, %2$s a écrit : Afficher la conversation + Lundi après-midi + Lundi matin Votre boîte est pleine et vous ne pouvez plus recevoir de nouveaux messages 🥲 Vous pourrez prochainement débloquer les limites de votre adresse mail ik.me (stockage limite, envoi, redirection, …) Votre boîte est pleine ? Obtenez plus de stockage ! @@ -356,6 +379,7 @@ %d de nouveaux messages Rédigez votre message + Lundi prochain Ce message est vide. Aucune conversation sélectionnée dans %s Vous n’avez pas encore de dossier… @@ -403,6 +427,8 @@ Aimez-vous Infomaniak Mail ? Enregistrer sur l’appareil Enregistrer dans kDrive + Programmer l’envoi + Cet e-mail sera envoyé à cette date : %s Messages programmés Tous les messages Rechercher un message @@ -411,7 +437,9 @@ Lus Non lus Recherche + Sélectionner la date Aucune signature + Sélectionner l\'heure Envoyer Messages envoyés Couleur d’accentuation @@ -525,6 +553,9 @@ Numéro copié dans le presse-papiers Signalement effectué avec succès Lancement de la restauration + Votre message a été sauvegardé dans les brouillons. + Le message sera envoyé le %s + Le message est en cours de planification Expéditeur blacklisté avec succès Expéditeurs blacklistés avec succès @@ -553,6 +584,8 @@ Pour pouvoir télécharger l’application kSync sur le Play Store, cliquez sur \"Installer\" puis revenez sur votre application Mail. Installée Activez les calendriers et carnets d’adresses dans kSync, que vous souhaitez synchroniser sur votre téléphone. + Cet après-midi + Ce soir Êtes-vous sûr de vouloir supprimer ce message définitivement ? Êtes-vous sûr de vouloir supprimer ces messages définitivement ? @@ -585,6 +618,7 @@ Mes comptes À : + Demain matin Vous ne pouvez pas ajouter cette adresse car vous avez atteint la limite de destinataires Corbeille (inconnu) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 8fcb19c6db..b5d85f7745 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -133,6 +133,7 @@ Crea Crea un account Aggiungi una cartella + Programma personalizzato stacca l’indirizzo Scarica Scarica tutti @@ -147,6 +148,7 @@ Accedi con un altro account Accesso Forse + Modificare Altro Ottieni più spazio di archiviazione Nuovo messaggio @@ -154,8 +156,10 @@ Apri nel mio calendario Rifiuta Richiesta password + Riprogrammare Ripristino delle e-mail Programmare l’invio di un’e-mail in un secondo momento + Programma Vedi Tutti Condividi @@ -185,6 +189,7 @@ Me Contatti Account selezionato + Icona di azione Indietro Cancella %s Cancella la bozza @@ -215,6 +220,9 @@ Risposta Stato di conservazione Azioni di apertura dei messaggi + Icona del programma + Messaggio programmato + Data programmata Selezionato Questa cartella contiene i messaggi non letti Avatar utente @@ -231,12 +239,17 @@ Nome della cartella Nessuno Cartella superiore + Scegli una data e un’ora Data: La cancellazione dell’account sarà definitiva. Non sarà possibile riattivare l’account. + Presto sarai in grado di sbloccare i limiti del tuo indirizzo email ik.me\n(limite di archiviazione, pianificazione personalizzata, invio e altro...) + Hai bisogno di più spazio di archiviazione e funzionalità? Per risolvere i problemi di visualizzazione, aggiornare l’applicazione Android System Webview. Problema di visualizzazione delle email Bozze (Bozza) + Programmazione annullata. Questo messaggio verrà spostato nelle tue bozze per essere inviato quando vuoi. + Modifica invio Stai per inviare un messaggio senza oggetto. Vuoi continuare? Oggetto vuoto Non c’è nulla da vedere qui. @@ -258,6 +271,7 @@ È richiesto almeno un destinatario Allegato non trovato Si è verificato un errore nell’invio della risposta + Scegli una data prossima Impossibile gestire la bozza, un allegato è danneggiato Bozza non trovata Impossibile modificare un messaggio pianificato o inviato @@ -279,6 +293,10 @@ Il nome della cartella non deve superare i 255 caratteri Il server respinge tutti i destinatari Il server respinge il mittente + + Non puoi programmare l\'invio di una e-mail tra meno di %d minuto + Non puoi programmare l’invio di una e-mail tra meno di %d minuti + Limite d’invio raggiunto Troppi destinatari Impossibile creare la cartella @@ -307,6 +325,8 @@ Da: I servizi Google Play sono necessari Posta in arrivo + Ultimo programma selezionato + Più tardi stamattina e Caricamento… L’accesso al tuo indirizzo e-mail è attualmente bloccato.\nPer ulteriori informazioni, leggi le FAQ. @@ -327,6 +347,8 @@ Bozza Il %1$s, %2$s ha scritto: Visualizza la conversazione + Lunedì pomeriggio + Lunedì mattina La casella di posta elettronica è piena e non è possibile ricevere altri messaggi 🥲 Presto potrete sbloccare i limiti del vostro indirizzo e-mail ik.me (limite di archiviazione, invio, inoltro, …). Lo spazio è esaurito? Ottieni più spazio! @@ -348,6 +370,7 @@ %d nuovi messaggi Digitare il messaggio + Lunedì prossimo Questo messaggio è vuoto. Nessuna conversazione selezionata in %s Non hai ancora nessuna cartella… @@ -395,6 +418,8 @@ Ti piace la posta di Infomaniak? Salva sul dispositivo Salva in kDrive + Pianifica l’invio + Questa email verrà inviata in questa data: %s Messaggi programmati Tutti i messaggi Ricerca di un’e-mail @@ -403,7 +428,9 @@ Leggi Non letti Ricerca + Selezionare la data Nessuna firma + Selezionare l\'ora Invia Messaggi inviati Colore d’accento @@ -517,6 +544,9 @@ Numero copiato negli appunti Segnalato con successo Avvio del restauro + Il tuo messaggio è stato salvato nelle bozze. + Il messaggio verrà inviato il %s + Il messaggio è in fase di programmazione Mittente inserito nella lista nera con successo Mittenti inseriti nella lista nera con successo @@ -542,6 +572,8 @@ Per scaricare l’applicazione kSync dal Play Store, fare clic su \"Installa\" e poi tornare all’applicazione Mail. Installata Attivare in kSync i calendari e le rubriche che si desidera sincronizzare con il telefono. + Questo pomeriggio + Questa sera Sei sicuro di voler cancellare questo messaggio in modo permanente? Sei sicuro di voler cancellare questi messaggi in modo permanente? @@ -571,6 +603,7 @@ I miei account A: + Domani mattina Non è possibile aggiungere questo indirizzo perché è stato raggiunto il limite di destinatari Cestino (sconosciuto) diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 1728d5d1fc..5ec10453cf 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -69,6 +69,7 @@ @color/elephant @color/yellow_dark @color/bat + @color/coral_dark @color/orca diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index ab48e1a612..41619ebaeb 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -61,8 +61,10 @@ - - + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 463d5fb6a5..fe594c820d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -169,6 +169,7 @@ @color/shark @color/yellow_light @color/white + @color/coral_light @color/mouse diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index abbf4e4431..f7ea9bc4f7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -139,6 +139,7 @@ Create Create an account Add a folder + Custom schedule detach mailbox Download Download all @@ -153,6 +154,7 @@ Log in with another account Login Maybe + Modify More Get more storage New message @@ -160,8 +162,10 @@ Open in my calendar Refuse Request password + Reschedule Restore emails Schedule email to send later + Schedule See All Share @@ -191,6 +195,7 @@ Me Contacts Selected account + Action icon Back Delete %s Delete draft @@ -221,6 +226,9 @@ Reply Storage status Open mail actions + Schedule icon + Scheduled message + Scheduled date Selected This folder contains unread mails User avatar @@ -237,12 +245,17 @@ Name of the folder None Parent folder + Choose a date and time Date: The deletion of your account will be final. You will not be able to reactivate your account. + You will soon be able to unlock the limits of your ik.me email address (storage limit, custom schedule, sending and more...) + Need more storage and features? To fix display problems, please update the Android System Webview application. Problem displaying your emails Drafts (Draft) + Programming cancelled. This message will be moved to your drafts to be sent whenever you want. + Edit Send You are about to send a message without a subject. Do you want to continue? Empty subject There is nothing to see here. @@ -264,6 +277,7 @@ At least one recipient is required Attachment cannot be found An error has occurred while sending your answer + Select an upcoming date Failed to handle draft, an attachment is corrupted Draft cannot be found Scheduled or sent messages cannot be edited @@ -285,6 +299,10 @@ The folder name should not exceed 255 characters The server is refusing all recipients The server is refusing the sender + + You can’t schedule an email less than %d minute in the future + You can’t schedule an email less than %d minutes in the future + Sending limit reached Too many recipients Unable to create the folder @@ -313,6 +331,8 @@ From: Google Play Services are required Inbox + Last selected schedule + Later this morning and Loading… Access to your mailbox is currently blocked.\nFor further information, please read FAQ. @@ -333,6 +353,8 @@ Draft On %1$s, %2$s wrote: Display the conversation + Monday afternoon + Monday morning Your mailbox is full and you can‘t receive any more messages 🥲 You will soon be able to unblock the limits of your ik.me email address (storage limit, sending, forwarding, …) Is your box full? Get more storage! @@ -354,6 +376,7 @@ %d new messages Type your message + Next monday This message is empty. No conversation selected in %s You don’t have any folder yet… @@ -401,6 +424,8 @@ Do you like Infomaniak Mail? Save to device Save to kDrive + Schedule sending + This email will be sent on this date: %s Scheduled messages All messages Search an email @@ -409,7 +434,9 @@ Read Unread Search + Select date No signature + Select time Send Sent messages Accent color @@ -523,6 +550,9 @@ Number copied to clipboard Successfully reported Launching the restoration + Your message has been saved in drafts. + The message will be sent on %s + The message is being scheduled Sender successfully blacklisted Senders successfully blacklisted @@ -548,6 +578,8 @@ To download the kSync application from the Play Store, click on \"Install\", then return to your Mail application. Installed Activate the calendars and address books in kSync that you wish to synchronize with your phone. + This afternoon + This evening Are you sure you want to delete this message permanently? Are you sure you want to delete these messages permanently? @@ -577,6 +609,7 @@ My accounts To: + Tomorrow morning You can’t add this address because you have reached the limit of recipients Trash (unknown) From f05cdf316dfeed5f68c41a8d40da66f2fcf3acd0 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 23 Jan 2025 14:35:26 +0100 Subject: [PATCH 02/70] fix: Use `tomorrow` instead of `getTomorrow` --- .../mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt index 6717354a5d..71eeecef44 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt @@ -210,7 +210,7 @@ enum class Schedule( TOMORROW_MORNING( R.string.tomorrowMorning, R.drawable.ic_morning_schedule, - Date().getTomorrow().getMorning(), + Date().tomorrow().getMorning(), listOf(TimeToDisplay.NIGHT, TimeToDisplay.MORNING, TimeToDisplay.AFTERNOON, TimeToDisplay.EVENING), "tomorrowMorning", ), From 6d7f7c44126ec606f59099057664267c0f349d40 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 23 Jan 2025 14:44:36 +0100 Subject: [PATCH 03/70] fix: Add missing DB bump --- .../main/java/com/infomaniak/mail/data/cache/RealmDatabase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/RealmDatabase.kt b/app/src/main/java/com/infomaniak/mail/data/cache/RealmDatabase.kt index 82c7153315..613a9bdab7 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/RealmDatabase.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/RealmDatabase.kt @@ -160,7 +160,7 @@ object RealmDatabase { //region Configurations versions const val USER_INFO_SCHEMA_VERSION = 2L - const val MAILBOX_INFO_SCHEMA_VERSION = 7L + const val MAILBOX_INFO_SCHEMA_VERSION = 8L const val MAILBOX_CONTENT_SCHEMA_VERSION = 20L //endregion From c90a6284846646e447d93e12d8382571f977720b Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 24 Jan 2025 13:25:37 +0100 Subject: [PATCH 04/70] refactor: Invert `visibleFoldersOnly` default value --- .../mail/data/cache/mailboxContent/FolderController.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/FolderController.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/FolderController.kt index 2c04d793de..2e86ae9276 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/FolderController.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/FolderController.kt @@ -50,7 +50,6 @@ class FolderController @Inject constructor( mailboxContentRealm(), withoutTypes = listOf(FoldersType.CUSTOM), withoutChildren = true, - visibleFoldersOnly = true, ).asFlow() } @@ -59,12 +58,11 @@ class FolderController @Inject constructor( mailboxContentRealm(), withoutTypes = listOf(FoldersType.DEFAULT), withoutChildren = true, - visibleFoldersOnly = true, ).asFlow() } fun getSearchFoldersAsync(): Flow> { - return getFoldersQuery(mailboxContentRealm(), withoutChildren = true, visibleFoldersOnly = true).asFlow() + return getFoldersQuery(mailboxContentRealm(), withoutChildren = true).asFlow() } fun getMoveFolders(): RealmResults { @@ -72,7 +70,6 @@ class FolderController @Inject constructor( mailboxContentRealm(), withoutTypes = listOf(FoldersType.SCHEDULED_DRAFTS, FoldersType.DRAFT), withoutChildren = true, - visibleFoldersOnly = true, ).find() } @@ -168,7 +165,7 @@ class FolderController @Inject constructor( realm: TypedRealm, withoutTypes: List = emptyList(), withoutChildren: Boolean = false, - visibleFoldersOnly: Boolean = false, + visibleFoldersOnly: Boolean = true, ): RealmQuery { val rootsQuery = if (withoutChildren) " AND $isRootFolder" else "" val typeQuery = withoutTypes.joinToString(separator = "") { @@ -195,7 +192,7 @@ class FolderController @Inject constructor( //region Get data private fun getFolders(exceptionsFoldersIds: List = emptyList(), realm: TypedRealm): RealmResults { val realmQuery = if (exceptionsFoldersIds.isEmpty()) { - getFoldersQuery(realm) + getFoldersQuery(realm, visibleFoldersOnly = false) } else { getFoldersQuery(exceptionsFoldersIds, realm) } From a021ee7ea7266430e03979ffda7523ea8135fe50 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 24 Jan 2025 13:26:26 +0100 Subject: [PATCH 05/70] fix: Fix DB bump issue with default value in a new column --- .../infomaniak/mail/data/cache/RealmMigrations.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/RealmMigrations.kt b/app/src/main/java/com/infomaniak/mail/data/cache/RealmMigrations.kt index cb6dd7c90f..6c3e3ffead 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/RealmMigrations.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/RealmMigrations.kt @@ -38,6 +38,7 @@ val MAILBOX_INFO_MIGRATION = AutomaticSchemaMigration { migrationContext -> val MAILBOX_CONTENT_MIGRATION = AutomaticSchemaMigration { migrationContext -> SentryDebug.addMigrationBreadcrumb(migrationContext) migrationContext.deleteRealmFromFirstMigration() + migrationContext.keepDefaultValuesAfterNineteenthMigration() } // Migrate to version #1 @@ -45,6 +46,7 @@ private fun MigrationContext.deleteRealmFromFirstMigration() { if (oldRealm.schemaVersion() < 1L) newRealm.deleteAll() } +//region Use default property values when adding a new column in a migration /** * Migrate from version #6 * @@ -73,3 +75,16 @@ private fun MigrationContext.keepDefaultValuesAfterSixthMigration() { } } } + +// Migrate from version #19 +private fun MigrationContext.keepDefaultValuesAfterNineteenthMigration() { + if (oldRealm.schemaVersion() <= 19L) { + enumerate(className = "Folder") { oldObject: DynamicRealmObject, newObject: DynamicMutableRealmObject? -> + newObject?.apply { + // Add property with default value + set(propertyName = "isDisplayed", value = true) + } + } + } +} +//endregion From 078892610bd0551cf6716e8e852f3de487fb34a8 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 24 Jan 2025 13:41:04 +0100 Subject: [PATCH 06/70] feat: Display correct recipient avatar & name in Scheduled drafts folder --- .../com/infomaniak/mail/data/models/thread/Thread.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/models/thread/Thread.kt b/app/src/main/java/com/infomaniak/mail/data/models/thread/Thread.kt index ffd2422c2b..56706b84d3 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/thread/Thread.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/thread/Thread.kt @@ -242,12 +242,14 @@ class Thread : RealmObject { fun computeAvatarRecipient(): Pair = runCatching { - val message = messages - .lastOrNull { it.folder.role != FolderRole.SENT && it.folder.role != FolderRole.DRAFT } - ?: messages.last() + val message = messages.lastOrNull { + it.folder.role != FolderRole.SENT && + it.folder.role != FolderRole.DRAFT && + it.folder.role != FolderRole.SCHEDULED_DRAFTS + } ?: messages.last() val recipients = when (message.folder.role) { - FolderRole.SENT, FolderRole.DRAFT -> message.to + FolderRole.SENT, FolderRole.DRAFT, FolderRole.SCHEDULED_DRAFTS -> message.to else -> message.from } @@ -269,7 +271,7 @@ class Thread : RealmObject { } fun computeDisplayedRecipients(): RealmList = when (folder.role) { - FolderRole.SENT, FolderRole.DRAFT -> to + FolderRole.SENT, FolderRole.DRAFT, FolderRole.SCHEDULED_DRAFTS -> to else -> from } From cf820b19c8db2be58a6c54a40b40aa1391760189 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 24 Jan 2025 14:03:14 +0100 Subject: [PATCH 07/70] docs: Add some explanatory comment --- .../com/infomaniak/mail/data/models/draft/Draft.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/models/draft/Draft.kt b/app/src/main/java/com/infomaniak/mail/data/models/draft/Draft.kt index c664d37739..2c0b4e14a7 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/draft/Draft.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/draft/Draft.kt @@ -72,8 +72,10 @@ class Draft : RealmObject { var swissTransferUuid: String? = null /** - * This `delay` should be set to 0 when `scheduleDate` is NOT set. - * Otherwise, if we remove it, we won't receive any `etop` from the API when sending an Email. + * We can't have both `delay` & `scheduleDate`. They are mutually exclusive. + * + * `delay` should be set to 0 when `scheduleDate` is not set. + * If we don't set it to 0, we won't receive any `etop` from the API when sending an Email. */ @EncodeDefault(EncodeDefault.Mode.NEVER) @Serializable(with = ConditionalIntSerializer::class) @@ -84,12 +86,14 @@ class Draft : RealmObject { } /** + * We can't have both `delay` & `scheduleDate`. They are mutually exclusive. + * * The API requires that this field does not exist, except when scheduling a message. * We use a custom serializer for this very reason. */ - @SerialName("schedule_date") @EncodeDefault(EncodeDefault.Mode.NEVER) @Serializable(with = ConditionalStringSerializer::class) + @SerialName("schedule_date") var scheduleDate: String? = null set(value) { delay = null From c46a89da7a746d70abb6ae9ae5a007de36a910c2 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 24 Jan 2025 17:28:22 +0100 Subject: [PATCH 08/70] fix: The application is now able to create Drafts again --- .../java/com/infomaniak/mail/workers/DraftsActionsWorker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bfcbf8a2ca..f1e31e9a0e 100644 --- a/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt +++ b/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt @@ -104,7 +104,7 @@ class DraftsActionsWorker @AssistedInject constructor( mailboxId = inputData.getIntOrNull(MAILBOX_ID_KEY) ?: return@withContext Result.failure() draftLocalUuid = inputData.getString(DRAFT_LOCAL_UUID_KEY) - scheduleDate = inputData.getLongOrNull(SCHEDULE_DATE_KEY) ?: return@withContext Result.failure() + scheduleDate = inputData.getLongOrNull(SCHEDULE_DATE_KEY) scheduleAction = inputData.getString(SCHEDULE_ACTION_KEY) userApiToken = AccountUtils.getUserById(userId)?.apiToken?.accessToken ?: return@withContext Result.failure() From e74ac79508b5769f4dafa0fdce2a8268eeff26f6 Mon Sep 17 00:00:00 2001 From: TommyDL-Infomaniak Date: Fri, 24 Jan 2025 17:36:52 +0100 Subject: [PATCH 09/70] feat: Improve DraftAction logic --- .../mail/ui/newMessage/NewMessageActivity.kt | 12 ++---------- .../mail/ui/newMessage/NewMessageFragment.kt | 2 +- .../mail/ui/newMessage/NewMessageViewModel.kt | 5 ++--- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt index 2f5d3fda1a..ffb94726b7 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -124,16 +124,8 @@ class NewMessageActivity : BaseActivity() { } private fun saveDraft() { - val draftAction = if (newMessageViewModel.shouldScheduleInsteadOfSend) { - DraftAction.SCHEDULE - } else if (newMessageViewModel.shouldSendInsteadOfSave) { - DraftAction.SEND - } else { - DraftAction.SAVE - } - val draftSaveConfiguration = DraftSaveConfiguration( - action = draftAction, + action = newMessageViewModel.draftAction, isFinishing = isFinishing, isTaskRoot = isTaskRoot, startWorkerCallback = ::startWorker, diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index a3da783766..651c33805a 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -734,7 +734,7 @@ class NewMessageFragment : Fragment() { } fun sendEmail() { - newMessageViewModel.shouldSendInsteadOfSave = true + newMessageViewModel.draftAction = if (scheduled) DraftAction.SCHEDULE else DraftAction.SEND setSnackbarActivityResult() requireActivity().finishAppAndRemoveTaskIfNeeded() } 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 3442b153f8..c5d83fcb8f 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 @@ -139,8 +139,7 @@ class NewMessageViewModel @Inject constructor( var isAutoCompletionOpened = false var isEditorExpanded = false var isExternalBannerManuallyClosed = false - var shouldScheduleInsteadOfSend = false - var shouldSendInsteadOfSave = false + var draftAction = DraftAction.SAVE var signaturesCount = 0 private var isNewMessage = false var scheduleDate: Date? = null @@ -868,7 +867,7 @@ class NewMessageViewModel @Inject constructor( val localUuid = draftLocalUuid ?: return@launch this@NewMessageViewModel.scheduleDate = scheduleDate - shouldScheduleInsteadOfSend = true + draftAction = DraftAction.SCHEDULE mailboxContentRealm().write { DraftController.getDraft(localUuid, realm = this)?.also { draft -> From 1734f72ef05b565fd5530d895e100e740aa45b44 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 24 Jan 2025 17:45:23 +0100 Subject: [PATCH 10/70] refactor: Rename stuff --- .../java/com/infomaniak/mail/data/api/ApiRepository.kt | 8 ++++---- .../main/java/com/infomaniak/mail/data/api/ApiRoutes.kt | 4 ++-- ...{SendScheduleDraftResult.kt => ScheduleDraftResult.kt} | 2 +- .../com/infomaniak/mail/workers/DraftsActionsWorker.kt | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) rename app/src/main/java/com/infomaniak/mail/data/models/draft/{SendScheduleDraftResult.kt => ScheduleDraftResult.kt} (96%) 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 4e7ac4e582..8ca1bd2417 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 @@ -42,8 +42,8 @@ import com.infomaniak.mail.data.models.correspondent.Contact import com.infomaniak.mail.data.models.correspondent.Recipient import com.infomaniak.mail.data.models.draft.Draft import com.infomaniak.mail.data.models.draft.SaveDraftResult +import com.infomaniak.mail.data.models.draft.ScheduleDraftResult import com.infomaniak.mail.data.models.draft.SendDraftResult -import com.infomaniak.mail.data.models.draft.SendScheduleDraftResult import com.infomaniak.mail.data.models.getMessages.ActivitiesResult import com.infomaniak.mail.data.models.getMessages.GetMessagesByUidsResult import com.infomaniak.mail.data.models.getMessages.NewMessagesResult @@ -193,13 +193,13 @@ object ApiRepository : ApiRepositoryCore() { return draft.remoteUuid?.let(::putDraft) ?: run(::postDraft) } - fun sendScheduleDraft(mailboxUuid: String, draft: Draft, okHttpClient: OkHttpClient): ApiResponse { + fun scheduleDraft(mailboxUuid: String, draft: Draft, okHttpClient: OkHttpClient): ApiResponse { val body = getDraftBody(draft) - fun postDraft(): ApiResponse = callApi(ApiRoutes.draft(mailboxUuid), POST, body, okHttpClient) + fun postDraft(): ApiResponse = callApi(ApiRoutes.draft(mailboxUuid), POST, body, okHttpClient) - fun putDraft(uuid: String): ApiResponse = + fun putDraft(uuid: String): ApiResponse = callApi(ApiRoutes.draft(mailboxUuid, uuid), PUT, body, okHttpClient) return draft.remoteUuid?.let(::putDraft) ?: run(::postDraft) diff --git a/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt b/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt index 89cc52f1cb..1911ff20ce 100644 --- a/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt +++ b/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt @@ -228,12 +228,12 @@ object ApiRoutes { } fun scheduleDraft(scheduleAction: String): String { - return "$MAIL_API$scheduleAction" + return "${MAIL_API}${scheduleAction}" } fun rescheduleDraft(draftResource: String, scheduleDate: Date): String { val formatedDate = scheduleDate.format(FORMAT_SCHEDULE_MAIL) - return "$MAIL_API$draftResource/schedule?schedule_date=${URLEncoder.encode(formatedDate, "UTF-8")}" + return "${MAIL_API}${draftResource}/schedule?schedule_date=${URLEncoder.encode(formatedDate, "UTF-8")}" } fun createAttachment(mailboxUuid: String): String { diff --git a/app/src/main/java/com/infomaniak/mail/data/models/draft/SendScheduleDraftResult.kt b/app/src/main/java/com/infomaniak/mail/data/models/draft/ScheduleDraftResult.kt similarity index 96% rename from app/src/main/java/com/infomaniak/mail/data/models/draft/SendScheduleDraftResult.kt rename to app/src/main/java/com/infomaniak/mail/data/models/draft/ScheduleDraftResult.kt index 04d7e0b80d..369087f9ce 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/draft/SendScheduleDraftResult.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/draft/ScheduleDraftResult.kt @@ -22,7 +22,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class SendScheduleDraftResult( +data class ScheduleDraftResult( val uuid: String, @SerialName("schedule_action") val scheduleAction: String, 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 f1e31e9a0e..d093a7c32b 100644 --- a/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt +++ b/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt @@ -376,7 +376,7 @@ class DraftsActionsWorker @AssistedInject constructor( } } - suspend fun executeScheduleSendAction() = with(ApiRepository.sendScheduleDraft(mailboxUuid, draft, okHttpClient)) { + suspend fun executeScheduleSendAction() = with(ApiRepository.scheduleDraft(mailboxUuid, draft, okHttpClient)) { when { isSuccess() -> { scheduleAction = data?.scheduleAction From fb3260fe8f4eac89e0cb73461ade3be6d019713d Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 24 Jan 2025 17:59:10 +0100 Subject: [PATCH 11/70] refactor: Update comments --- app/src/main/java/com/infomaniak/mail/data/models/Folder.kt | 2 +- .../java/com/infomaniak/mail/data/models/message/Message.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt b/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt index 2487bb59a4..fa2cd2f188 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt @@ -93,7 +93,7 @@ class Folder : RealmObject, Cloneable { @Transient var sortedName: String = name @Transient - var isDisplayed: Boolean = true + var isDisplayed: Boolean = true // Used to hide folders on specific conditions (i.e. ScheduledDrafts folder when its empty) //endregion private val _parents by backlinks(Folder::children) 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 8b281d8b00..f873848d1e 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 @@ -117,7 +117,7 @@ class Message : RealmObject { // ------------- !IMPORTANT! ------------- // Every field that is added in this Transient region should be declared in 'initLocalValue()' too // to avoid loosing data when updating from API. - // If the Field is a "heavy data" (i.e an embedded object), it should also be added in 'keepHeavyData()' + // If the Field is a "heavy data" (i.e. an embedded object), it should also be added in 'keepHeavyData()' @Transient @PersistedName("isFullyDownloaded") From dbd57c84e96c59ce936e5c221497873fde6850ea Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 24 Jan 2025 18:05:36 +0100 Subject: [PATCH 12/70] refactor: Remove unused fields --- .../infomaniak/mail/data/models/draft/ScheduleDraftResult.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/models/draft/ScheduleDraftResult.kt b/app/src/main/java/com/infomaniak/mail/data/models/draft/ScheduleDraftResult.kt index 369087f9ce..c976b4236c 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/draft/ScheduleDraftResult.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/draft/ScheduleDraftResult.kt @@ -23,8 +23,6 @@ import kotlinx.serialization.Serializable @Serializable data class ScheduleDraftResult( - val uuid: String, @SerialName("schedule_action") val scheduleAction: String, - val uid: String, ) From 95eb3055a5ed9d2aa5ebad0fb60b6f79fc65ffa8 Mon Sep 17 00:00:00 2001 From: TommyDL-Infomaniak Date: Tue, 28 Jan 2025 15:46:43 +0100 Subject: [PATCH 13/70] chore: Update translations --- app/src/main/res/values-de/strings.xml | 3 ++- app/src/main/res/values-es/strings.xml | 3 ++- app/src/main/res/values-fr/strings.xml | 3 ++- app/src/main/res/values-it/strings.xml | 3 ++- app/src/main/res/values/strings.xml | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 206aea7d5a..d05ca55bec 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -248,7 +248,7 @@ Problem mit der Anzeige Ihrer E-Mails Entwürfe (Entwurf) - Programmierung abgebrochen. Diese Nachricht wird in Ihre Entwürfe verschoben, damit Sie sie senden können, wann immer Sie möchten. + Diese Nachricht wird in Ihre Entwürfe verschoben, damit Sie sie senden können, wann immer Sie möchten. Senden bearbeiten Sie sind dabei, eine Nachricht ohne Betreff zu senden. Möchten Sie fortfahren? Leeres Thema @@ -418,6 +418,7 @@ Mögen Sie Infomaniak Mail? Auf dem Gerät speichern In kDrive speichern + Zeitplan abgesagt. Planen Sie den Versand Diese E-Mail wird an diesem Datum gesendet: %s Geplante Nachrichten diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 1dbe7c5638..2f8e56f47b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -248,7 +248,7 @@ Problema de visualización de sus emails Borradores (Borrador) - Programmazione annullata. Questo messaggio verrà spostato nelle tue bozze per essere inviato quando vuoi. + Este mensaje se moverá a tus borradores para que puedas enviarlo cuando lo desees. Editar envío Está a punto de enviar un mensaje sin asunto. ¿Desea continuar? Asunto vacío @@ -419,6 +419,7 @@ ¿Te gusta Infomaniak Mail? Guardar en el dispositivo Guardar en kDrive + Horario cancelado. Programar envío Este correo electrónico se enviará en esta fecha: %s Mensajes programados diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4cefc15cd5..932fc15089 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -250,7 +250,7 @@ Problème d’affichage de vos e-mails Brouillons (Brouillon) - Programmation annulée. Ce message sera déplacé dans vos brouillons pour être envoyé quand vous le souhaitez. + Ce message sera déplacé dans vos brouillons pour être envoyé quand vous le souhaitez. Modifier l’envoi Vous êtes sur le point d’envoyer un message sans objet. Voulez-vous continuer ? Objet vide @@ -427,6 +427,7 @@ Aimez-vous Infomaniak Mail ? Enregistrer sur l’appareil Enregistrer dans kDrive + Programmation annulée. Programmer l’envoi Cet e-mail sera envoyé à cette date : %s Messages programmés diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b5d85f7745..1bd2eb17b6 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -248,7 +248,7 @@ Problema di visualizzazione delle email Bozze (Bozza) - Programmazione annullata. Questo messaggio verrà spostato nelle tue bozze per essere inviato quando vuoi. + Questo messaggio verrà spostato nelle tue bozze per essere inviato quando vuoi. Modifica invio Stai per inviare un messaggio senza oggetto. Vuoi continuare? Oggetto vuoto @@ -418,6 +418,7 @@ Ti piace la posta di Infomaniak? Salva sul dispositivo Salva in kDrive + Programma annullato. Pianifica l’invio Questa email verrà inviata in questa data: %s Messaggi programmati diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7ea9bc4f7..4772f2cdbc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -254,7 +254,7 @@ Problem displaying your emails Drafts (Draft) - Programming cancelled. This message will be moved to your drafts to be sent whenever you want. + This message will be moved to your drafts to be sent whenever you want. Edit Send You are about to send a message without a subject. Do you want to continue? Empty subject @@ -424,6 +424,7 @@ Do you like Infomaniak Mail? Save to device Save to kDrive + Schedule cancelled. Schedule sending This email will be sent on this date: %s Scheduled messages From 80b33646838da4e255aa2ac20bfcd1cead2372b8 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 29 Jan 2025 08:58:05 +0100 Subject: [PATCH 14/70] docs: Add explanatory comment --- .../java/com/infomaniak/mail/data/models/message/Message.kt | 3 +++ 1 file changed, 3 insertions(+) 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 f873848d1e..6b26551a3e 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 @@ -80,6 +80,7 @@ class Message : RealmObject { var isDraft: Boolean = false @SerialName("draft_resource") var draftResource: String? = null + // Boolean used to know if this Message is scheduled to be sent sometime in the future, or not @SerialName("is_scheduled_draft") var isScheduledDraft: Boolean = false var body: Body? = null @@ -94,6 +95,8 @@ class Message : RealmObject { var isAnswered: Boolean = false @SerialName("flagged") var isFavorite: Boolean = false + // Boolean used to know if this Message is currently being sent, but can + // still be cancelled during 10~30 sec, depending on user configuration @SerialName("scheduled") var isScheduled: Boolean = false var preview: String = "" From 31521758db21801c64e2d7c434a67eebdfa235bb Mon Sep 17 00:00:00 2001 From: TommyDL-Infomaniak Date: Wed, 29 Jan 2025 14:32:47 +0100 Subject: [PATCH 15/70] chore: Remove redundant scheduleDraft API route --- .../main/java/com/infomaniak/mail/data/api/ApiRepository.kt | 4 ++-- app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) 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 8ca1bd2417..78af1c0bf8 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 @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -253,7 +253,7 @@ object ApiRepository : ApiRepositoryCore() { } fun deleteScheduleDraft(scheduleAction: String): ApiResponse { - return callApi(ApiRoutes.scheduleDraft(scheduleAction), DELETE) + return callApi(ApiRoutes.resource(scheduleAction), DELETE) } fun rescheduleDraft(draftResource: String, scheduleDate: Date): ApiResponse { diff --git a/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt b/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt index 1911ff20ce..4e7ef9464f 100644 --- a/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt +++ b/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2025 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -227,10 +227,6 @@ object ApiRoutes { return "${draft(mailboxUuid)}/$remoteDraftUuid" } - fun scheduleDraft(scheduleAction: String): String { - return "${MAIL_API}${scheduleAction}" - } - fun rescheduleDraft(draftResource: String, scheduleDate: Date): String { val formatedDate = scheduleDate.format(FORMAT_SCHEDULE_MAIL) return "${MAIL_API}${draftResource}/schedule?schedule_date=${URLEncoder.encode(formatedDate, "UTF-8")}" From 2e6c205a78a83097ef6c3746048713788eb2f665 Mon Sep 17 00:00:00 2001 From: TommyDL-Infomaniak Date: Wed, 29 Jan 2025 14:48:07 +0100 Subject: [PATCH 16/70] feat: Hide the section title in SCHEDULED_DRAFTS folder --- .../mail/ui/main/folder/ThreadListAdapter.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt index 6bcba6ee1e..a19ba27738 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt @@ -676,13 +676,17 @@ class ThreadListAdapter @Inject constructor( var previousSectionTitle = "" threads.forEach { thread -> scope.ensureActive() - val sectionTitle = thread.getSectionTitle(context) - when { - sectionTitle != previousSectionTitle -> { - add(sectionTitle) - previousSectionTitle = sectionTitle + + if (folderRole != FolderRole.SCHEDULED_DRAFTS) { + val sectionTitle = thread.getSectionTitle(context) + when { + sectionTitle != previousSectionTitle -> { + add(sectionTitle) + previousSectionTitle = sectionTitle + } } } + add(thread) } } From 776ae460596deab7c12ccc4e7edae11b969d7a5a Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 30 Jan 2025 10:02:43 +0100 Subject: [PATCH 17/70] chore: Update strings --- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 4 ++-- app/src/main/res/values-fr/strings.xml | 6 +++--- app/src/main/res/values-it/strings.xml | 6 +++--- app/src/main/res/values/strings.xml | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d05ca55bec..5520f090ba 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -242,7 +242,7 @@ Wählen Sie ein Datum und eine Uhrzeit Datum: Die Löschung Ihres Kontos ist endgültig. Sie werden nicht in der Lage sein, Ihr Konto zu reaktivieren. - In Kürze können Sie die Beschränkungen Ihrer ik.me-E-Mail-Adresse entsperren\n(Speicherlimit, benutzerdefinierter Zeitplan, Senden und mehr ...) + In Kürze können Sie die Beschränkungen Ihrer ik.me-E-Mail-Adresse entsperren\n(Speicherlimit, benutzerdefinierter Zeitplan, Senden und mehr…) Benötigen Sie mehr Speicherplatz und Funktionen? Um Anzeigeprobleme zu beheben, aktualisieren Sie bitte die Anwendung Android System Webview. Problem mit der Anzeige Ihrer E-Mails diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2f8e56f47b..50672d64a2 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -242,7 +242,7 @@ Elige una fecha y hora Date: La eliminación de su cuenta será definitiva. No podrá reactivar su cuenta. - Pronto podrás desbloquear los límites de tu dirección de correo electrónico ik.me\n(límite de almacenamiento, programación personalizada, envío y más...) + Pronto podrás desbloquear los límites de tu dirección de correo electrónico ik.me\n(límite de almacenamiento, programación personalizada, envío y más…) ¿Necesita más almacenamiento y funciones? Para solucionar los problemas de visualización, actualice la aplicación Android System Webview. Problema de visualización de sus emails @@ -432,7 +432,7 @@ Buscar en Seleccione la fecha Sin firma - Selezionare l\'ora + Seleccionar la hora Enviar Mensajes enviados Color de acento diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 932fc15089..96347b8794 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -191,7 +191,7 @@ Moi Contacts Compte sélectionné - Icône d\'action + Icône d’action Retour Supprimer %s Supprimer le brouillon @@ -244,7 +244,7 @@ Choisir une date et une heure Date : La suppression de votre compte sera définitive. Vous ne pourrez pas réactiver votre compte. - Vous pourrez prochainement débloquer les limites de votre adresse mail ik.me \n(limite de stockage, horaire personnalisé, envoi et plus encore...) + Vous pourrez prochainement débloquer les limites de votre adresse mail ik.me\n(limite de stockage, horaire personnalisé, envoi et plus encore…) Besoin de plus de stockage et de fonctionnalités ? Pour corriger les problèmes d’affichage, veuillez mettre à jour l’application Android System Webview. Problème d’affichage de vos e-mails @@ -440,7 +440,7 @@ Recherche Sélectionner la date Aucune signature - Sélectionner l\'heure + Sélectionner l’heure Envoyer Messages envoyés Couleur d’accentuation diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 1bd2eb17b6..492404e7ac 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -242,7 +242,7 @@ Scegli una data e un’ora Data: La cancellazione dell’account sarà definitiva. Non sarà possibile riattivare l’account. - Presto sarai in grado di sbloccare i limiti del tuo indirizzo email ik.me\n(limite di archiviazione, pianificazione personalizzata, invio e altro...) + Presto sarai in grado di sbloccare i limiti del tuo indirizzo email ik.me\n(limite di archiviazione, pianificazione personalizzata, invio e altro…) Hai bisogno di più spazio di archiviazione e funzionalità? Per risolvere i problemi di visualizzazione, aggiornare l’applicazione Android System Webview. Problema di visualizzazione delle email @@ -294,7 +294,7 @@ Il server respinge tutti i destinatari Il server respinge il mittente - Non puoi programmare l\'invio di una e-mail tra meno di %d minuto + Non puoi programmare l’invio di una e-mail tra meno di %d minuto Non puoi programmare l’invio di una e-mail tra meno di %d minuti Limite d’invio raggiunto @@ -431,7 +431,7 @@ Ricerca Selezionare la data Nessuna firma - Selezionare l\'ora + Selezionare l’ora Invia Messaggi inviati Colore d’accento diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4772f2cdbc..b76cf1482f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -248,7 +248,7 @@ Choose a date and time Date: The deletion of your account will be final. You will not be able to reactivate your account. - You will soon be able to unlock the limits of your ik.me email address (storage limit, custom schedule, sending and more...) + You will soon be able to unlock the limits of your ik.me email address\n(storage limit, custom schedule, sending and more…) Need more storage and features? To fix display problems, please update the Android System Webview application. Problem displaying your emails From c7f664fedf5f0dd2558647c42f4e3c4db0975c39 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 30 Jan 2025 10:57:34 +0100 Subject: [PATCH 18/70] refactor: Update UI --- .../res/layout/item_bottom_sheet_action.xml | 17 +++-- app/src/main/res/layout/item_message.xml | 71 ++++++++++--------- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/app/src/main/res/layout/item_bottom_sheet_action.xml b/app/src/main/res/layout/item_bottom_sheet_action.xml index cc352847a8..c690101e4d 100644 --- a/app/src/main/res/layout/item_bottom_sheet_action.xml +++ b/app/src/main/res/layout/item_bottom_sheet_action.xml @@ -29,8 +29,8 @@ android:id="@+id/divider" android:layout_width="0dp" android:layout_height="1dp" + android:layout_marginHorizontal="@dimen/marginStandardMedium" android:layout_marginVertical="0dp" - android:layout_marginStart="@dimen/marginStandardMedium" app:dividerColor="@color/dividerColor" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -43,10 +43,10 @@ android:layout_marginVertical="@dimen/marginStandardMedium" android:layout_marginStart="@dimen/marginStandardMedium" android:contentDescription="@string/contentDescriptionScheduleIcon" - android:src="@drawable/ic_afternoon_schedule" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/ic_afternoon_schedule" /> + app:layout_constraintStart_toEndOf="@id/icon" + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/restoreEmailsTitle" /> - + android:visibility="gone" + tools:src="@drawable/ic_chevron_right" /> diff --git a/app/src/main/res/layout/item_message.xml b/app/src/main/res/layout/item_message.xml index defca7bc0d..a404a7d6f7 100644 --- a/app/src/main/res/layout/item_message.xml +++ b/app/src/main/res/layout/item_message.xml @@ -67,7 +67,6 @@ app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toEndOf="@id/userAvatar" app:layout_constraintTop_toTopOf="parent" - app:layout_goneMarginEnd="@dimen/marginStandardVerySmall" tools:text="@tools:sample/full_names" /> + + + + + + - - - - Date: Thu, 30 Jan 2025 11:02:21 +0100 Subject: [PATCH 19/70] fix: Remove wrongly added `+` in `@id` --- .idea/navEditor.xml | 144 ++++++++++++------ .../main/res/layout/activity_no_mailbox.xml | 2 +- .../main/res/layout/cardview_thread_item.xml | 20 +-- .../main/res/layout/fragment_new_account.xml | 6 +- .../main/res/layout/fragment_new_message.xml | 6 +- .../res/layout/item_menu_drawer_footer.xml | 2 +- app/src/main/res/layout/item_message.xml | 8 +- .../main/res/layout/layout_attachments.xml | 2 +- .../res/layout/view_avatar_name_email.xml | 6 +- app/src/main/res/layout/view_main_actions.xml | 12 +- .../main/res/navigation/main_navigation.xml | 2 +- 11 files changed, 129 insertions(+), 81 deletions(-) diff --git a/.idea/navEditor.xml b/.idea/navEditor.xml index 533687d6a1..a584185378 100644 --- a/.idea/navEditor.xml +++ b/.idea/navEditor.xml @@ -112,7 +112,7 @@ @@ -124,8 +124,8 @@ @@ -136,7 +136,7 @@ @@ -148,7 +148,7 @@ @@ -160,7 +160,7 @@ @@ -172,7 +172,7 @@ @@ -193,7 +193,7 @@ @@ -205,7 +205,7 @@ @@ -217,7 +217,7 @@ @@ -229,7 +229,7 @@ @@ -241,7 +241,7 @@ @@ -253,7 +253,7 @@ @@ -265,7 +265,7 @@ @@ -291,7 +291,7 @@ @@ -303,7 +303,7 @@ @@ -315,7 +315,7 @@ @@ -327,7 +327,7 @@ @@ -339,7 +339,7 @@ @@ -363,7 +363,7 @@ @@ -375,7 +375,7 @@ @@ -387,7 +387,7 @@ @@ -399,7 +399,7 @@ @@ -430,7 +430,7 @@ @@ -454,7 +454,7 @@ @@ -466,7 +466,7 @@ @@ -499,7 +499,7 @@ @@ -511,7 +511,7 @@ @@ -523,7 +523,7 @@ @@ -547,7 +547,19 @@ + + + + + + + @@ -559,7 +571,7 @@ @@ -580,7 +592,7 @@ @@ -606,7 +618,7 @@ @@ -682,7 +694,7 @@ @@ -694,7 +706,7 @@ @@ -706,7 +718,7 @@ @@ -718,7 +730,7 @@ @@ -739,7 +751,7 @@ @@ -751,7 +763,7 @@ @@ -763,7 +775,7 @@ @@ -775,7 +787,7 @@ @@ -787,7 +799,7 @@ @@ -843,7 +855,7 @@ @@ -855,13 +867,25 @@ + + + + + + + @@ -889,7 +913,7 @@ @@ -901,7 +925,7 @@ @@ -913,7 +937,7 @@ @@ -937,7 +961,7 @@ @@ -963,6 +987,30 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_no_mailbox.xml b/app/src/main/res/layout/activity_no_mailbox.xml index 8a868f7fed..809739422e 100644 --- a/app/src/main/res/layout/activity_no_mailbox.xml +++ b/app/src/main/res/layout/activity_no_mailbox.xml @@ -66,7 +66,7 @@ android:id="@+id/noMailboxIconLayout" android:layout_width="0dp" android:layout_height="250dp" - app:layout_constraintBottom_toBottomOf="@+id/bottomWave" + app:layout_constraintBottom_toBottomOf="@id/bottomWave" app:layout_constraintEnd_toStartOf="@id/end" app:layout_constraintStart_toEndOf="@id/start" app:lottie_autoPlay="true" diff --git a/app/src/main/res/layout/cardview_thread_item.xml b/app/src/main/res/layout/cardview_thread_item.xml index f93c127ea3..9739132cda 100644 --- a/app/src/main/res/layout/cardview_thread_item.xml +++ b/app/src/main/res/layout/cardview_thread_item.xml @@ -29,7 +29,7 @@ android:layout_height="wrap_content" android:layout_marginVertical="2dp" android:layout_marginStart="@dimen/marginStandardVerySmall" - android:nextFocusRight="@+id/newMessageFab" + android:nextFocusRight="@id/newMessageFab" app:cardCornerRadius="@null" app:cardPreventCornerOverlap="false" app:shapeAppearanceOverlay="@style/RoundedDecoratedTextItemShapeAppearance"> @@ -171,7 +171,7 @@ android:ellipsize="end" android:lines="1" app:layout_constrainedWidth="true" - app:layout_constraintEnd_toStartOf="@+id/threadCountCard" + app:layout_constraintEnd_toStartOf="@id/threadCountCard" app:layout_constraintStart_toEndOf="@id/draftPrefix" app:layout_constraintTop_toTopOf="parent" tools:text="@tools:sample/full_names" /> @@ -224,9 +224,9 @@ android:contentDescription="@string/contentDescriptionScheduleSendIcon" android:src="@drawable/ic_scheduled_messages" android:visibility="gone" - app:layout_constraintBottom_toBottomOf="@+id/mailDate" - app:layout_constraintEnd_toStartOf="@+id/mailDate" - app:layout_constraintTop_toTopOf="@+id/mailDate" + app:layout_constraintBottom_toBottomOf="@id/mailDate" + app:layout_constraintEnd_toStartOf="@id/mailDate" + app:layout_constraintTop_toTopOf="@id/mailDate" app:tint="@color/scheduledIconColor" tools:visibility="visible" /> @@ -246,9 +246,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="@id/mailSubject" - app:layout_constraintEnd_toStartOf="@+id/mailSubject" + app:layout_constraintEnd_toStartOf="@id/mailSubject" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@+id/mailSubject"> + app:layout_constraintTop_toTopOf="@id/mailSubject"> + app:layout_constraintBottom_toTopOf="@id/externalBanner" + app:layout_constraintTop_toBottomOf="@id/toolbar"> diff --git a/app/src/main/res/layout/item_message.xml b/app/src/main/res/layout/item_message.xml index a404a7d6f7..e77b423835 100644 --- a/app/src/main/res/layout/item_message.xml +++ b/app/src/main/res/layout/item_message.xml @@ -98,10 +98,10 @@ android:contentDescription="@string/contentDescriptionScheduleSendIcon" android:src="@drawable/ic_scheduled_messages" android:visibility="gone" - app:layout_constraintBottom_toBottomOf="@+id/shortMessageDate" + app:layout_constraintBottom_toBottomOf="@id/shortMessageDate" app:layout_constraintEnd_toStartOf="@id/shortMessageDate" - app:layout_constraintStart_toEndOf="@+id/iconsSpace" - app:layout_constraintTop_toTopOf="@+id/shortMessageDate" + app:layout_constraintStart_toEndOf="@id/iconsSpace" + app:layout_constraintTop_toTopOf="@id/shortMessageDate" app:tint="@color/scheduledIconColor" tools:visibility="visible" /> @@ -113,7 +113,7 @@ android:layout_marginEnd="@dimen/marginStandardSmall" app:layout_constraintBaseline_toBaselineOf="@id/expeditorName" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/scheduleSendIcon" + app:layout_constraintStart_toEndOf="@id/scheduleSendIcon" tools:text="9 déc 2021 à 11:00" /> + app:layout_constraintTop_toBottomOf="@id/attachmentsRecyclerView"> diff --git a/app/src/main/res/layout/view_main_actions.xml b/app/src/main/res/layout/view_main_actions.xml index 5e6c1f2d67..24308b1412 100644 --- a/app/src/main/res/layout/view_main_actions.xml +++ b/app/src/main/res/layout/view_main_actions.xml @@ -33,7 +33,7 @@ app:cornerRadius="8dp" app:iconSize="24dp" app:iconTint="?android:attr/colorPrimary" - app:layout_constraintEnd_toStartOf="@+id/button2" + app:layout_constraintEnd_toStartOf="@id/button2" app:layout_constraintHorizontal_chainStyle="spread" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -66,8 +66,8 @@ app:iconTint="?android:attr/colorPrimary" app:layout_constraintBottom_toBottomOf="@id/button1" app:layout_constraintDimensionRatio="1" - app:layout_constraintEnd_toStartOf="@+id/button3" - app:layout_constraintStart_toEndOf="@+id/button1" + app:layout_constraintEnd_toStartOf="@id/button3" + app:layout_constraintStart_toEndOf="@id/button1" app:layout_constraintTop_toTopOf="parent" tools:icon="@drawable/ic_email_action_reply_to_all" /> @@ -98,8 +98,8 @@ app:iconTint="?android:attr/colorPrimary" app:layout_constraintBottom_toBottomOf="@id/button1" app:layout_constraintDimensionRatio="1" - app:layout_constraintEnd_toStartOf="@+id/button4" - app:layout_constraintStart_toEndOf="@+id/button2" + app:layout_constraintEnd_toStartOf="@id/button4" + app:layout_constraintStart_toEndOf="@id/button2" app:layout_constraintTop_toTopOf="parent" tools:icon="@drawable/ic_email_action_send" /> @@ -131,7 +131,7 @@ app:layout_constraintBottom_toBottomOf="@id/button1" app:layout_constraintDimensionRatio="1" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/button3" + app:layout_constraintStart_toEndOf="@id/button3" app:layout_constraintTop_toTopOf="parent" tools:icon="@drawable/ic_bin" /> diff --git a/app/src/main/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml index ef43f6c14e..2ed7354e5d 100644 --- a/app/src/main/res/navigation/main_navigation.xml +++ b/app/src/main/res/navigation/main_navigation.xml @@ -53,7 +53,7 @@ + app:destination="@id/attachMailboxFragment" /> From e42298c91584b88ba12244bdab00a2eab067e064 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 30 Jan 2025 11:29:29 +0100 Subject: [PATCH 20/70] fix: Add missing copyrights --- .../main/res/drawable/ic_afternoon_schedule.xml | 17 +++++++++++++++++ app/src/main/res/drawable/ic_arrow_return.xml | 17 +++++++++++++++++ .../main/res/drawable/ic_evening_schedule.xml | 17 +++++++++++++++++ .../res/drawable/ic_last_schedule_selected.xml | 17 +++++++++++++++++ .../main/res/drawable/ic_morning_schedule.xml | 17 +++++++++++++++++ .../drawable/ic_morning_sunrise_schedule.xml | 17 +++++++++++++++++ app/src/main/res/drawable/ic_pen.xml | 17 +++++++++++++++++ app/src/main/res/drawable/ic_schedule_send.xml | 17 +++++++++++++++++ .../res/layout/bottom_sheet_upgrade_product.xml | 2 +- 9 files changed, 137 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/drawable/ic_afternoon_schedule.xml b/app/src/main/res/drawable/ic_afternoon_schedule.xml index ee7943e1da..03530e5f56 100644 --- a/app/src/main/res/drawable/ic_afternoon_schedule.xml +++ b/app/src/main/res/drawable/ic_afternoon_schedule.xml @@ -1,3 +1,20 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_update_logo.xml b/app/src/main/res/drawable/ic_update_logo.xml index 5b2509e3e9..f91cc39159 100644 --- a/app/src/main/res/drawable/ic_update_logo.xml +++ b/app/src/main/res/drawable/ic_update_logo.xml @@ -1,6 +1,6 @@ + android:width="263dp" + android:height="194dp" + android:viewportWidth="263" + android:viewportHeight="194"> + + + + + + android:fillType="evenOdd" + android:pathData="M114.7,112.7C114.7,111.49 115.89,110.63 117.07,110.99L137.17,117.04C137.94,117.27 138.46,117.96 138.46,118.75V135.58C138.46,136.78 137.27,137.64 136.1,137.29L115.99,131.23C115.22,131 114.7,130.31 114.7,129.52V112.7Z" /> - + + android:fillType="evenOdd" + android:pathData="M114.7,109.66L138.46,116.82L126.51,127.23C125.62,128.01 124.22,127.76 123.67,126.72L114.7,109.66Z" /> + android:fillColor="#FF8500" + android:pathData="M252.56,71L251.35,64.2L247.87,64.82L249.07,71.62L242.18,70.3L241.61,73.7L248.47,74.85L245.46,81.15L248.83,82.56L251.69,76.1L256.94,81.12L259.22,78.66L254.29,73.81L260.46,70.36L258.58,67.4L252.56,71Z" /> + + android:pathData="M83.52,44.19L84.51,39.54L82.14,39.04L81.16,43.69L77.14,41.08L75.9,43.1L79.95,45.59L76.43,48.84L78.2,50.6L81.67,47.22L83.7,51.77L85.77,50.78L83.9,46.43L88.68,45.81L88.25,43.43L83.52,44.19Z" /> + android:fillColor="#FFC10A" + android:pathData="M159.6,7.82V0.92H156.06V7.82L149.51,5.33L148.35,8.58L154.9,10.9L150.84,16.58L153.92,18.56L157.86,12.7L162.15,18.56L164.82,16.53L160.82,10.9L167.49,8.58L166.16,5.33L159.6,7.82Z" /> + android:pathData="M252.43,124.65V120.34H250.22V124.65L246.12,123.09L245.39,125.13L249.49,126.58L246.96,130.14L248.88,131.37L251.35,127.7L254.03,131.37L255.7,130.1L253.2,126.58L257.37,125.13L256.54,123.09L252.43,124.65Z" /> + + + + + + + + + android:fillColor="#F1F1F1" + android:pathData="M194.67,79.36C193.52,80.52 189.45,80.41 189.45,80.41C189.45,80.41 189.34,76.34 190.5,75.19C191.65,74.04 193.52,74.04 194.67,75.19C195.82,76.34 195.82,78.21 194.67,79.36Z" /> From 8be66024d29f253bb1984fbf2cfa776fe57b7763 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 5 Feb 2025 12:57:43 +0100 Subject: [PATCH 53/70] fix: Delete duplicated `Sent` icon --- .../com/infomaniak/mail/data/models/Folder.kt | 2 +- .../main/res/drawable/ic_sent_messages.xml | 26 ------------------- 2 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_sent_messages.xml diff --git a/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt b/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt index fa2cd2f188..7613be4413 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/Folder.kt @@ -168,7 +168,7 @@ class Folder : RealmObject, Cloneable { INBOX(R.string.inboxFolder, R.drawable.ic_drawer_inbox, 9, "inboxFolder"), COMMERCIAL(R.string.commercialFolder, R.drawable.ic_promotions, 8, "commercialFolder"), SOCIALNETWORKS(R.string.socialNetworksFolder, R.drawable.ic_social_media, 7, "socialNetworksFolder"), - SENT(R.string.sentFolder, R.drawable.ic_sent_messages, 6, "sentFolder"), + SENT(R.string.sentFolder, R.drawable.ic_send, 6, "sentFolder"), SCHEDULED_DRAFTS(R.string.scheduledMessagesFolder, R.drawable.ic_schedule_send, 5, "scheduledDraftsFolder"), DRAFT(R.string.draftFolder, R.drawable.ic_draft, 4, "draftFolder"), SPAM(R.string.spamFolder, R.drawable.ic_spam, 3, "spamFolder"), diff --git a/app/src/main/res/drawable/ic_sent_messages.xml b/app/src/main/res/drawable/ic_sent_messages.xml deleted file mode 100644 index 6e2d5f9ed5..0000000000 --- a/app/src/main/res/drawable/ic_sent_messages.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - From f8f242fe5dd5bb4a1c688ed1f6f934bbbea0a3c3 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 6 Feb 2025 07:08:17 +0100 Subject: [PATCH 54/70] feat: Disable MultiSelection in Scheduled Drafts folder --- .../mail/ui/main/folder/ThreadListAdapter.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt index 3128fb75f7..21fec49a9a 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListAdapter.kt @@ -106,6 +106,8 @@ class ThreadListAdapter @Inject constructor( private set //endregion + private val isMultiselectDisabledInThisFolder: Boolean get() = folderRole == FolderRole.SCHEDULED_DRAFTS + init { setHasStableIds(true) } @@ -243,7 +245,7 @@ class ThreadListAdapter @Inject constructor( iconFavorite.isVisible = isFavorite val messagesCount = messages.count() - threadCountText.text = messagesCount.toString() + threadCountText.text = "$messagesCount" threadCountCard.isVisible = messagesCount > 1 if (unseenMessagesCount == 0) setThreadUiRead() else setThreadUiUnread() @@ -253,13 +255,15 @@ class ThreadListAdapter @Inject constructor( multiSelection?.let { listener -> selectionCardView.setOnLongClickListener { - onThreadClickWithAbilityToOpenMultiSelection(thread, listener, TrackerAction.LONG_PRESS) + onThreadClickWithAbilityToOpenMultiSelection(thread, position, listener, TrackerAction.LONG_PRESS) true } expeditorAvatar.apply { - setOnClickListener { onThreadClickWithAbilityToOpenMultiSelection(thread, listener, TrackerAction.CLICK) } + setOnClickListener { + onThreadClickWithAbilityToOpenMultiSelection(thread, position, listener, TrackerAction.CLICK) + } setOnLongClickListener { - onThreadClickWithAbilityToOpenMultiSelection(thread, listener, TrackerAction.LONG_PRESS) + onThreadClickWithAbilityToOpenMultiSelection(thread, position, listener, TrackerAction.LONG_PRESS) true } } @@ -311,9 +315,15 @@ class ThreadListAdapter @Inject constructor( private fun CardviewThreadItemBinding.onThreadClickWithAbilityToOpenMultiSelection( thread: Thread, + position: Int, listener: MultiSelectionListener, action: TrackerAction, ) { + if (isMultiselectDisabledInThisFolder) { + onThreadClicked(thread, position) + return + } + val hasOpened = openMultiSelectionIfClosed(listener, action) toggleMultiSelectedThread(thread, shouldUpdateSelectedUi = !hasOpened) } From 45e12aa3bc589dc84bb333675ea24a07326fc69d Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 6 Feb 2025 07:56:53 +0100 Subject: [PATCH 55/70] feat: Add actions buttons in ThreadAdapter when in Scheduled Drafts folder --- .../java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 01c6ae414b..ec40a78fd5 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 @@ -644,11 +644,11 @@ class ThreadAdapter( setOnClickListener { threadAdapterCallbacks?.onDeleteDraftClicked?.invoke(message) } } replyButton.apply { - isVisible = isExpanded + isVisible = isExpanded && message.isScheduledDraft.not() setOnClickListener { threadAdapterCallbacks?.onReplyClicked?.invoke(message) } } menuButton.apply { - isVisible = isExpanded + isVisible = isExpanded && message.isScheduledDraft.not() setOnClickListener { threadAdapterCallbacks?.onMenuClicked?.invoke(message) } } From 0c59aa1be263ddb218a9e48e516dfa98a2d68d7c Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 6 Feb 2025 09:48:11 +0100 Subject: [PATCH 56/70] feat: Only keep Delete action in Thread `quickActionBar` for Scheduled Drafts threads --- .../mail/ui/main/thread/ThreadFragment.kt | 7 +++--- .../mail/views/BottomQuickActionBarView.kt | 13 +++++++++-- app/src/main/res/layout/fragment_thread.xml | 5 +--- .../main/res/menu/scheduled_draft_menu.xml | 23 +++++++++++++++++++ 4 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 app/src/main/res/menu/scheduled_draft_menu.xml diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt index eebd1872e3..d758b72fe2 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt @@ -412,7 +412,7 @@ class ThreadFragment : Fragment() { mainViewModel.toggleLightThemeForMessage.observe(viewLifecycleOwner, threadAdapter::toggleLightMode) } - private fun observeThreadLive() { + private fun observeThreadLive() = with(binding) { threadViewModel.threadLive.observe(viewLifecycleOwner) { thread -> @@ -421,7 +421,7 @@ class ThreadFragment : Fragment() { return@observe } - binding.iconFavorite.apply { + iconFavorite.apply { setIconResource(if (thread.isFavorite) R.drawable.ic_star_filled else R.drawable.ic_star) val color = if (thread.isFavorite) { context.getColor(R.color.favoriteYellow) @@ -431,7 +431,8 @@ class ThreadFragment : Fragment() { iconTint = ColorStateList.valueOf(color) } - binding.quickActionBar.isVisible = thread.numberOfScheduledDrafts != thread.messages.size + val shouldDisplayScheduledDraftActions = thread.numberOfScheduledDrafts == thread.messages.size + quickActionBar.init(if (shouldDisplayScheduledDraftActions) R.menu.scheduled_draft_menu else R.menu.message_menu) } } diff --git a/app/src/main/java/com/infomaniak/mail/views/BottomQuickActionBarView.kt b/app/src/main/java/com/infomaniak/mail/views/BottomQuickActionBarView.kt index 3b7ef5e881..cce1fed226 100644 --- a/app/src/main/java/com/infomaniak/mail/views/BottomQuickActionBarView.kt +++ b/app/src/main/java/com/infomaniak/mail/views/BottomQuickActionBarView.kt @@ -25,6 +25,7 @@ import android.view.MenuInflater import android.widget.ActionMenuView import android.widget.FrameLayout import androidx.annotation.DrawableRes +import androidx.annotation.MenuRes import androidx.annotation.StringRes import androidx.core.content.res.getResourceIdOrThrow import androidx.core.view.get @@ -37,7 +38,7 @@ import com.infomaniak.mail.databinding.ViewBottomQuickActionBarBinding class BottomQuickActionBarView @JvmOverloads constructor( context: Context, - attrs: AttributeSet? = null, + val attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : FrameLayout(context, attrs, defStyleAttr) { @@ -47,8 +48,16 @@ class BottomQuickActionBarView @JvmOverloads constructor( private val menu: Menu by lazy { ActionMenuView(context).menu } init { + init() + } + + fun init(@MenuRes menuRes: Int? = null) { attrs?.getAttributes(context, R.styleable.BottomQuickActionBarView) { - MenuInflater(context).inflate(getResourceIdOrThrow(R.styleable.BottomQuickActionBarView_menu), menu) + + val menuResId = menuRes + ?: runCatching { getResourceIdOrThrow(R.styleable.BottomQuickActionBarView_menu) }.getOrNull() + ?: return@getAttributes + MenuInflater(context).inflate(menuResId, menu) buttons.forEachIndexed { index, button -> if (index >= menu.size) { diff --git a/app/src/main/res/layout/fragment_thread.xml b/app/src/main/res/layout/fragment_thread.xml index 17941604b5..d5f3a7b31a 100644 --- a/app/src/main/res/layout/fragment_thread.xml +++ b/app/src/main/res/layout/fragment_thread.xml @@ -121,13 +121,10 @@ android:id="@+id/quickActionBar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/threadCoordinatorLayout" - app:menu="@menu/message_menu" - tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@id/threadCoordinatorLayout" /> + + + From e146349c5815e0985ba119e515a6fcf3811919e5 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 6 Feb 2025 11:19:51 +0100 Subject: [PATCH 57/70] refactor: Various renaming to better differentiate between scheduled Message & scheduled Draft --- .../com/infomaniak/mail/data/LocalSettings.kt | 2 +- .../mail/data/cache/RealmMigrations.kt | 10 ++++ .../cache/mailboxContent/MessageController.kt | 6 +-- .../mail/data/models/FeatureFlag.kt | 2 +- .../mail/data/models/draft/SendDraftResult.kt | 2 +- .../models/getMessages/ActivitiesResult.kt | 2 +- .../mail/data/models/message/Message.kt | 4 +- .../com/infomaniak/mail/ui/MainActivity.kt | 4 +- .../com/infomaniak/mail/ui/MainViewModel.kt | 4 +- .../ScheduleSendBottomSheetDialog.kt | 8 +-- .../mail/ui/main/thread/ThreadFragment.kt | 4 +- .../mail/ui/newMessage/NewMessageActivity.kt | 2 +- .../mail/ui/newMessage/NewMessageFragment.kt | 6 +-- .../mail/ui/newMessage/NewMessageViewModel.kt | 12 ++--- .../mail/utils/extensions/Extensions.kt | 2 +- .../mail/workers/DraftsActionsWorker.kt | 50 ++++++++++--------- 16 files changed, 65 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt b/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt index 89b1275de1..c57d4f0816 100644 --- a/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt +++ b/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt @@ -69,7 +69,7 @@ class LocalSettings private constructor(context: Context) : SharedValues { var autoAdvanceNaturalThread by sharedValue("autoAdvanceNaturalThreadKey", AutoAdvanceMode.FOLLOWING_THREAD) var showWebViewOutdated by sharedValue("showWebViewOutdatedKey", true) var accessTokenApiCallRecord by sharedValue("accessTokenApiCallRecordKey", null) - var lastSelectedScheduleEpoch by sharedValue("lastSelectedSchedule", null) + var lastSelectedScheduleEpoch by sharedValue("lastSelectedScheduleEpochKey", null) fun removeSettings() = sharedPreferences.transaction { clear() } diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/RealmMigrations.kt b/app/src/main/java/com/infomaniak/mail/data/cache/RealmMigrations.kt index 456ef67c4d..4a1a4bab3b 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/RealmMigrations.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/RealmMigrations.kt @@ -78,13 +78,23 @@ private fun MigrationContext.keepDefaultValuesAfterSixthMigration() { // Migrate from version #19 private fun MigrationContext.keepDefaultValuesAfterNineteenthMigration() { + if (oldRealm.schemaVersion() <= 19L) { + enumerate(className = "Folder") { _: DynamicRealmObject, newObject: DynamicMutableRealmObject? -> newObject?.apply { // Add property with default value set(propertyName = "isDisplayed", value = true) } } + + enumerate(className = "Message") { oldObject: DynamicRealmObject, newObject: DynamicMutableRealmObject? -> + newObject?.apply { + // Rename property without losing its previous value + set(propertyName = "isScheduledMessage", value = oldObject.getValue(fieldName = "isScheduled")) + } + } + } } //endregion diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/MessageController.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/MessageController.kt index da14ab0d06..cbdfec54da 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/MessageController.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/MessageController.kt @@ -85,11 +85,11 @@ class MessageController @Inject constructor(private val mailboxContentRealm: Rea fun getMovableMessages(thread: Thread): List { val byFolderId = "${Message::folderId.name} == '${thread.folderId}'" - return getMessagesAndDuplicates(thread, "$byFolderId AND $isNotScheduled") + return getMessagesAndDuplicates(thread, "$byFolderId AND $isNotScheduledMessage") } fun getUnscheduledMessages(thread: Thread): List { - return getMessagesAndDuplicates(thread, isNotScheduled) + return getMessagesAndDuplicates(thread, isNotScheduledMessage) } private fun getMessagesAndDuplicates(thread: Thread, query: String): List { @@ -152,7 +152,7 @@ class MessageController @Inject constructor(private val mailboxContentRealm: Rea companion object { private val isNotDraft = "${Message::isDraft.name} == false" - private val isNotScheduled = "${Message::isScheduled.name} == false" + private val isNotScheduledMessage = "${Message::isScheduledMessage.name} == false" //region Queries private fun getMessagesQuery(messageUid: String, realm: TypedRealm): RealmQuery { diff --git a/app/src/main/java/com/infomaniak/mail/data/models/FeatureFlag.kt b/app/src/main/java/com/infomaniak/mail/data/models/FeatureFlag.kt index aac8fc9ca3..96474a41ec 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/FeatureFlag.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/FeatureFlag.kt @@ -21,5 +21,5 @@ package com.infomaniak.mail.data.models enum class FeatureFlag(val apiName: String) { AI("ai-mail-composer"), BIMI("bimi"), - SCHEDULE_SEND_DRAFT("schedule-send-draft"), + SCHEDULE_DRAFTS("schedule-send-draft"), } 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 01757292b0..4497c7146c 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 @@ -23,5 +23,5 @@ import kotlinx.serialization.Serializable @Serializable data class SendDraftResult( @SerialName("etop") - val etopScheduledDate: String, + val scheduledMessageEtop: String, ) diff --git a/app/src/main/java/com/infomaniak/mail/data/models/getMessages/ActivitiesResult.kt b/app/src/main/java/com/infomaniak/mail/data/models/getMessages/ActivitiesResult.kt index b4189d1d1b..e21825360d 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/getMessages/ActivitiesResult.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/getMessages/ActivitiesResult.kt @@ -44,7 +44,7 @@ data class ActivitiesResult( @SerialName("forwarded") val isForwarded: Boolean, @SerialName("scheduled") - val isScheduled: Boolean, + val isScheduledMessage: Boolean, @SerialName("seen") val isSeen: Boolean, ) 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 4887e18b88..80273f4d99 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 @@ -98,7 +98,7 @@ class Message : RealmObject { // Boolean used to know if this Message is currently being sent, but can // still be cancelled during 10~30 sec, depending on user configuration @SerialName("scheduled") - var isScheduled: Boolean = false + var isScheduledMessage: Boolean = false var preview: String = "" var size: Int = 0 @SerialName("has_unsubscribe_link") @@ -325,7 +325,7 @@ class Message : RealmObject { isFavorite = flags.isFavorite isAnswered = flags.isAnswered isForwarded = flags.isForwarded - isScheduled = flags.isScheduled + isScheduledMessage = flags.isScheduledMessage } fun shouldBeExpanded(index: Int, lastIndex: Int) = !isDraft && (!isSeen || index == lastIndex) 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 a4b674f7df..5136d53bbf 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt @@ -303,8 +303,8 @@ class MainActivity : BaseActivity() { val userId = getInt(DraftsActionsWorker.RESULT_USER_ID_KEY, 0) if (userId != AccountUtils.currentUserId) return - getLong(DraftsActionsWorker.BIGGEST_ETOP_SCHEDULED_DATE_KEY, 0).takeIf { it > 0 }?.let { etopScheduledDate -> - mainViewModel.refreshDraftFolderWhenDraftArrives(etopScheduledDate) + getLong(DraftsActionsWorker.BIGGEST_SCHEDULED_MESSAGE_ETOP_KEY, 0).takeIf { it > 0 }?.let { scheduledMessageEtop -> + mainViewModel.refreshDraftFolderWhenDraftArrives(scheduledMessageEtop) } } 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 63c5c906bf..fc05016380 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt @@ -1208,13 +1208,13 @@ class MainViewModel @Inject constructor( selectedThreadsLiveData.value = selectedThreads } - fun refreshDraftFolderWhenDraftArrives(etopScheduledDate: Long) = viewModelScope.launch(ioCoroutineContext) { + fun refreshDraftFolderWhenDraftArrives(scheduledMessageEtop: Long) = viewModelScope.launch(ioCoroutineContext) { val folder = folderController.getFolder(FolderRole.DRAFT) if (folder?.cursor != null) { val timeNow = Date().time - val delay = REFRESH_DELAY + max(etopScheduledDate - timeNow, 0L) + val delay = REFRESH_DELAY + max(scheduledMessageEtop - timeNow, 0L) delay(min(delay, MAX_REFRESH_DELAY)) refreshController.refreshThreads( diff --git a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt index b4fe4be728..0cc83213a7 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt @@ -96,12 +96,12 @@ class ScheduleSendBottomSheetDialog @Inject constructor() : ActionsBottomSheetDi if (navigationArgs.isAlreadyScheduled) { if (draftResource != null && lastSelectedScheduleEpoch != 0L) { trackScheduleSendEvent(matomoName) - setBackNavigationResult(SCHEDULE_SEND_RESULT, lastSelectedScheduleEpoch) + setBackNavigationResult(SCHEDULE_DRAFT_RESULT, lastSelectedScheduleEpoch) } } else { if (lastSelectedScheduleEpoch != 0L) { trackScheduleSendEvent(matomoName) - setBackNavigationResult(SCHEDULE_SEND_RESULT, lastSelectedScheduleEpoch) + setBackNavigationResult(SCHEDULE_DRAFT_RESULT, lastSelectedScheduleEpoch) } } } @@ -126,12 +126,12 @@ class ScheduleSendBottomSheetDialog @Inject constructor() : ActionsBottomSheetDi setIconResource(schedule.scheduleIconRes) setOnClickListener { trackScheduleSendEvent(schedule.matomoValue) - setBackNavigationResult(SCHEDULE_SEND_RESULT, schedule.date().time) + setBackNavigationResult(SCHEDULE_DRAFT_RESULT, schedule.date().time) } } companion object { - const val SCHEDULE_SEND_RESULT = "schedule_send_result" + const val SCHEDULE_DRAFT_RESULT = "schedule_draft_result" const val OPEN_DATE_AND_TIME_SCHEDULE_DIALOG = "open_date_and_time_schedule_dialog" } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt index d758b72fe2..d6820d6f91 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt @@ -69,7 +69,7 @@ import com.infomaniak.mail.databinding.FragmentThreadBinding import com.infomaniak.mail.ui.MainViewModel import com.infomaniak.mail.ui.alertDialogs.* import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialog.Companion.OPEN_DATE_AND_TIME_SCHEDULE_DIALOG -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialog.Companion.SCHEDULE_SEND_RESULT +import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialog.Companion.SCHEDULE_DRAFT_RESULT import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialogArgs import com.infomaniak.mail.ui.main.SnackbarManager import com.infomaniak.mail.ui.main.folder.TwoPaneFragment @@ -561,7 +561,7 @@ class ThreadFragment : Fragment() { ) } - getBackNavigationResult(SCHEDULE_SEND_RESULT) { selectedScheduleEpoch: Long -> + getBackNavigationResult(SCHEDULE_DRAFT_RESULT) { selectedScheduleEpoch: Long -> mainViewModel.rescheduleDraft(Date(selectedScheduleEpoch)) } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt index ffb94726b7..04e92211a4 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt @@ -137,7 +137,7 @@ class NewMessageActivity : BaseActivity() { private fun startWorker() { draftsActionsWorkerScheduler.scheduleWork( draftLocalUuid = newMessageViewModel.draftLocalUuid(), - scheduleDate = newMessageViewModel.scheduleDate, + scheduleDate = newMessageViewModel.scheduledDraftDate, ) } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 2a1825b378..91ccb4a173 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -65,7 +65,7 @@ import com.infomaniak.mail.ui.alertDialogs.DescriptionAlertDialog import com.infomaniak.mail.ui.alertDialogs.InformationAlertDialog import com.infomaniak.mail.ui.alertDialogs.SelectDateAndTimeForScheduledDraftDialog import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialog.Companion.OPEN_DATE_AND_TIME_SCHEDULE_DIALOG -import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialog.Companion.SCHEDULE_SEND_RESULT +import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialog.Companion.SCHEDULE_DRAFT_RESULT import com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialogArgs import com.infomaniak.mail.ui.main.SnackbarManager import com.infomaniak.mail.ui.main.thread.AttachmentAdapter @@ -242,7 +242,7 @@ class NewMessageFragment : Fragment() { ) } - getBackNavigationResult(SCHEDULE_SEND_RESULT) { selectedScheduleEpoch: Long -> + getBackNavigationResult(SCHEDULE_DRAFT_RESULT) { selectedScheduleEpoch: Long -> newMessageViewModel.setScheduleDate(Date(selectedScheduleEpoch)) tryToSendEmail(scheduled = true) } @@ -698,7 +698,7 @@ class NewMessageFragment : Fragment() { private fun observeScheduledDraftsFeatureFlagUpdates() { newMessageViewModel.currentMailboxLive.observeNotNull(viewLifecycleOwner) { mailbox -> - val isScheduledDraftsEnabled = mailbox.featureFlags.contains(FeatureFlag.SCHEDULE_SEND_DRAFT) + val isScheduledDraftsEnabled = mailbox.featureFlags.contains(FeatureFlag.SCHEDULE_DRAFTS) binding.scheduleSendButton.isVisible = isScheduledDraftsEnabled } } 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 8630ec8cb5..0568e59696 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 @@ -137,8 +137,8 @@ class NewMessageViewModel @Inject constructor( var isExternalBannerManuallyClosed = false var draftAction = DraftAction.SAVE var signaturesCount = 0 + var scheduledDraftDate: Date? = null private var isNewMessage = false - var scheduleDate: Date? = null private var snapshot: DraftSnapshot? = null @@ -154,7 +154,6 @@ class NewMessageViewModel @Inject constructor( val editorAction = SingleLiveEvent>() // Needs to trigger every time the Fragment is recreated val initResult = MutableLiveData() - val scheduleMessageTrigger = SingleLiveEvent() private val _isShimmering = MutableStateFlow(true) val isShimmering: StateFlow = _isShimmering @@ -196,7 +195,6 @@ class NewMessageViewModel @Inject constructor( fun draftLocalUuid() = draftLocalUuid fun draftMode() = draftMode fun shouldLoadDistantResources() = shouldLoadDistantResources - fun triggerScheduleMessage() = scheduleMessageTrigger.postValue(Unit) fun initDraftAndViewModel(intent: Intent): LiveData = liveData(ioCoroutineContext) { @@ -859,21 +857,21 @@ class NewMessageViewModel @Inject constructor( }.onFailure(Sentry::captureException) } - fun setScheduleDate(scheduleDate: Date) = viewModelScope.launch(ioDispatcher) { + fun setScheduleDate(date: Date) = viewModelScope.launch(ioDispatcher) { val localUuid = draftLocalUuid ?: return@launch - this@NewMessageViewModel.scheduleDate = scheduleDate + scheduledDraftDate = date draftAction = DraftAction.SCHEDULE mailboxContentRealm().write { DraftController.getDraft(localUuid, realm = this)?.also { draft -> - draft.scheduleDate = this@NewMessageViewModel.scheduleDate?.format(FORMAT_SCHEDULE_MAIL) + draft.scheduleDate = scheduledDraftDate?.format(FORMAT_SCHEDULE_MAIL) } } } fun resetScheduledDate() = viewModelScope.launch(ioDispatcher) { val localUuid = draftLocalUuid ?: return@launch - scheduleDate = null + scheduledDraftDate = null draftAction = DraftAction.SAVE mailboxContentRealm().write { diff --git a/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt b/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt index f4bcff7a9f..4a3dfe1ac3 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt @@ -347,7 +347,7 @@ private tailrec fun formatFolderWithAllChildren( /* * There are two types of folders: * - user's folders (with or without a role) - * - hidden IK folders (scheduled drafts, snoozed, etc…) + * - hidden IK folders (ScheduledDrafts, Snoozed, etc…) * * We want to display the user's folders, and also the IK folders for which we handle the role. * IK folders where we don't handle the role are dismissed. 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 15cdfa91ab..dbee5cf663 100644 --- a/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt +++ b/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt @@ -90,8 +90,10 @@ class DraftsActionsWorker @AssistedInject constructor( private lateinit var userApiToken: String private var isSnackbarFeedbackNeeded: Boolean = false - private var scheduleDate: Long? = null - private var scheduleAction: String? = null + //region Scheduled Drafts + private var scheduledDraftDate: Long? = null + private var scheduledDraftAction: String? = null + //endregion private val dateFormatWithTimezone by lazy { SimpleDateFormat(FORMAT_DATE_WITH_TIMEZONE, Locale.ROOT) } @@ -105,8 +107,8 @@ class DraftsActionsWorker @AssistedInject constructor( mailboxId = inputData.getIntOrNull(MAILBOX_ID_KEY) ?: return@withContext Result.failure() draftLocalUuid = inputData.getString(DRAFT_LOCAL_UUID_KEY) - scheduleDate = inputData.getLongOrNull(SCHEDULE_DATE_KEY) - scheduleAction = inputData.getString(SCHEDULE_ACTION_KEY) + scheduledDraftDate = inputData.getLongOrNull(SCHEDULE_DATE_KEY) + scheduledDraftAction = inputData.getString(SCHEDULE_ACTION_KEY) userApiToken = AccountUtils.getUserById(userId)?.apiToken?.accessToken ?: return@withContext Result.failure() mailbox = mailboxController.getMailbox(userId, mailboxId) ?: return@withContext Result.failure() @@ -137,7 +139,7 @@ class DraftsActionsWorker @AssistedInject constructor( // List containing the callback function to update/delete drafts in Realm // We keep these Realm changes in a List to execute them all in a unique Realm transaction at the end of the worker val realmActionsOnDraft = mutableListOf<(MutableRealm) -> Unit>() - val etopScheduledDates = mutableListOf() + val scheduledMessageEtops = mutableListOf() var trackedDraftErrorMessageResId: Int? = null var remoteUuidOfTrackedDraft: String? = null var trackedDraftAction: DraftAction? = null @@ -162,10 +164,10 @@ class DraftsActionsWorker @AssistedInject constructor( remoteUuidOfTrackedDraft = savedDraftUuid isTrackedDraftSuccess = true } - etopScheduledDate?.let(etopScheduledDates::add) + scheduledMessageEtop?.let(scheduledMessageEtops::add) realmActionOnDraft?.let(realmActionsOnDraft::add) - this@DraftsActionsWorker.scheduleAction = scheduleAction + scheduledDraftAction = scheduleAction } else if (isTargetDraft) { trackedDraftErrorMessageResId = errorMessageResId!! isTrackedDraftSuccess = false @@ -219,7 +221,7 @@ class DraftsActionsWorker @AssistedInject constructor( showDraftErrorNotification(isTrackedDraftSuccess, trackedDraftErrorMessageResId, trackedDraftAction) return computeResult( - etopScheduledDates, + scheduledMessageEtops, haveAllDraftsSucceeded, isTrackedDraftSuccess, remoteUuidOfTrackedDraft, @@ -260,7 +262,7 @@ class DraftsActionsWorker @AssistedInject constructor( } private fun computeResult( - etopScheduledDates: MutableList, + scheduledMessagesEtops: MutableList, haveAllDraftsSucceeded: Boolean, isTrackedDraftSuccess: Boolean?, remoteUuidOfTrackedDraft: String?, @@ -268,7 +270,7 @@ class DraftsActionsWorker @AssistedInject constructor( trackedDraftErrorMessageResId: Int?, ): Result { - val biggestEtopScheduledDate = etopScheduledDates.mapNotNull { dateFormatWithTimezone.parse(it)?.time }.maxOrNull() + val biggestScheduleMessageEtop = scheduledMessagesEtops.mapNotNull { dateFormatWithTimezone.parse(it)?.time }.maxOrNull() return if (haveAllDraftsSucceeded || isTrackedDraftSuccess == true) { val outputData = if (isSnackbarFeedbackNeeded) { @@ -276,10 +278,10 @@ class DraftsActionsWorker @AssistedInject constructor( REMOTE_DRAFT_UUID_KEY to draftLocalUuid?.let { remoteUuidOfTrackedDraft }, ASSOCIATED_MAILBOX_UUID_KEY to draftLocalUuid?.let { mailbox.uuid }, RESULT_DRAFT_ACTION_KEY to draftLocalUuid?.let { trackedDraftAction?.name }, - BIGGEST_ETOP_SCHEDULED_DATE_KEY to biggestEtopScheduledDate, + BIGGEST_SCHEDULED_MESSAGE_ETOP_KEY to biggestScheduleMessageEtop, RESULT_USER_ID_KEY to userId, - SCHEDULE_DATE_KEY to scheduleDate, - SCHEDULE_ACTION_KEY to scheduleAction, + SCHEDULE_DATE_KEY to scheduledDraftDate, + SCHEDULE_ACTION_KEY to scheduledDraftAction, ) } else { Data.EMPTY @@ -289,7 +291,7 @@ class DraftsActionsWorker @AssistedInject constructor( val outputData = if (isSnackbarFeedbackNeeded) { workDataOf( ERROR_MESSAGE_RESID_KEY to trackedDraftErrorMessageResId, - BIGGEST_ETOP_SCHEDULED_DATE_KEY to biggestEtopScheduledDate, + BIGGEST_SCHEDULED_MESSAGE_ETOP_KEY to biggestScheduleMessageEtop, RESULT_USER_ID_KEY to userId, ) } else { @@ -301,7 +303,7 @@ class DraftsActionsWorker @AssistedInject constructor( data class DraftActionResult( val realmActionOnDraft: ((MutableRealm) -> Unit)?, - val etopScheduledDate: String?, + val scheduledMessageEtop: String?, val scheduleAction: String?, val errorMessageResId: Int?, val savedDraftUuid: String?, @@ -311,8 +313,8 @@ class DraftsActionsWorker @AssistedInject constructor( private suspend fun executeDraftAction(draft: Draft, mailboxUuid: String, isFirstTime: Boolean = true): DraftActionResult { var realmActionOnDraft: ((MutableRealm) -> Unit)? = null - var etopScheduledDate: String? = null - var scheduleAction: String? = null + var scheduledMessageEtop: String? = null + var scheduleDraftAction: String? = null var savedDraftUuid: String? = null SentryDebug.addDraftBreadcrumbs(draft, step = "executeDraftAction (action = ${draft.action?.name.toString()})") @@ -331,7 +333,7 @@ class DraftsActionsWorker @AssistedInject constructor( return DraftActionResult( realmActionOnDraft = null, - etopScheduledDate = null, + scheduledMessageEtop = null, scheduleAction = null, errorMessageResId = R.string.errorCorruptAttachment, savedDraftUuid = null, @@ -348,7 +350,7 @@ class DraftsActionsWorker @AssistedInject constructor( action = null } } - etopScheduledDate = dateFormatWithTimezone.format(Date()) + scheduledMessageEtop = dateFormatWithTimezone.format(Date()) savedDraftUuid = data.draftRemoteUuid } ?: run { retryWithNewIdentityOrThrow(draft, mailboxUuid, isFirstTime) @@ -358,7 +360,7 @@ class DraftsActionsWorker @AssistedInject constructor( suspend fun executeSendAction() = with(ApiRepository.sendDraft(mailboxUuid, draft, okHttpClient)) { when { isSuccess() -> { - etopScheduledDate = data?.etopScheduledDate + scheduledMessageEtop = data?.scheduledMessageEtop realmActionOnDraft = deleteDraftCallback(draft) } error?.exception is SerializationException -> { @@ -378,7 +380,7 @@ class DraftsActionsWorker @AssistedInject constructor( suspend fun executeScheduleAction() = with(ApiRepository.scheduleDraft(mailboxUuid, draft, okHttpClient)) { when { isSuccess() -> { - scheduleAction = data?.scheduleAction + scheduleDraftAction = data?.scheduleAction realmActionOnDraft = deleteDraftCallback(draft) refreshScheduledDraftsFolder() } @@ -405,8 +407,8 @@ class DraftsActionsWorker @AssistedInject constructor( return DraftActionResult( realmActionOnDraft = realmActionOnDraft, - etopScheduledDate = etopScheduledDate, - scheduleAction = scheduleAction, + scheduledMessageEtop = scheduledMessageEtop, + scheduleAction = scheduleDraftAction, errorMessageResId = null, savedDraftUuid = savedDraftUuid, isSuccess = true, @@ -499,7 +501,7 @@ class DraftsActionsWorker @AssistedInject constructor( const val REMOTE_DRAFT_UUID_KEY = "remoteDraftUuidKey" const val ASSOCIATED_MAILBOX_UUID_KEY = "associatedMailboxUuidKey" const val RESULT_DRAFT_ACTION_KEY = "resultDraftActionKey" - const val BIGGEST_ETOP_SCHEDULED_DATE_KEY = "biggestEtopScheduledDateKey" + const val BIGGEST_SCHEDULED_MESSAGE_ETOP_KEY = "biggestScheduledMessageEtopKey" const val RESULT_USER_ID_KEY = "resultUserIdKey" const val SCHEDULE_DATE_KEY = "scheduleDateKey" const val SCHEDULE_ACTION_KEY = "scheduleActionKey" From 58e2c625c3453a813de9a4492d181f2d3f3726b4 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Thu, 6 Feb 2025 13:49:47 +0100 Subject: [PATCH 58/70] refactor: Simplify `resetScheduledDate()` code --- .../mail/ui/newMessage/NewMessageFragment.kt | 21 +++++++++---------- .../mail/ui/newMessage/NewMessageViewModel.kt | 16 +++----------- .../main/res/layout/fragment_new_message.xml | 2 +- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 91ccb4a173..07650c60cd 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -228,24 +228,25 @@ class NewMessageFragment : Fragment() { ) } + fun scheduleDraft(timestamp: Long) { + newMessageViewModel.setScheduleDate(Date(timestamp)) + tryToSendEmail(scheduled = true) + } + getBackNavigationResult(OPEN_DATE_AND_TIME_SCHEDULE_DIALOG) { _: Boolean -> dateAndTimeScheduleDialog.show( title = getString(R.string.datePickerTitle), onPositiveButtonClicked = { val scheduleDate = dateAndTimeScheduleDialog.selectedDate.time localSettings.lastSelectedScheduleEpoch = scheduleDate - newMessageViewModel.setScheduleDate(Date(scheduleDate)) - tryToSendEmail(scheduled = true) + scheduleDraft(scheduleDate) }, onNegativeButtonClicked = ::navigateBackToBottomSheet, onCancel = ::navigateBackToBottomSheet, ) } - getBackNavigationResult(SCHEDULE_DRAFT_RESULT) { selectedScheduleEpoch: Long -> - newMessageViewModel.setScheduleDate(Date(selectedScheduleEpoch)) - tryToSendEmail(scheduled = true) - } + getBackNavigationResult(SCHEDULE_DRAFT_RESULT, ::scheduleDraft) getBackNavigationResult(AttachmentExtensions.DOWNLOAD_ATTACHMENT_RESULT, ::startActivity) } @@ -699,7 +700,7 @@ class NewMessageFragment : Fragment() { private fun observeScheduledDraftsFeatureFlagUpdates() { newMessageViewModel.currentMailboxLive.observeNotNull(viewLifecycleOwner) { mailbox -> val isScheduledDraftsEnabled = mailbox.featureFlags.contains(FeatureFlag.SCHEDULE_DRAFTS) - binding.scheduleSendButton.isVisible = isScheduledDraftsEnabled + binding.scheduleButton.isVisible = isScheduledDraftsEnabled } } @@ -727,11 +728,11 @@ class NewMessageFragment : Fragment() { private fun setupSendButtons() = with(binding) { newMessageViewModel.isSendingAllowed.observe(viewLifecycleOwner) { - scheduleSendButton.isEnabled = it + scheduleButton.isEnabled = it sendButton.isEnabled = it } - scheduleSendButton.setOnClickListener { + scheduleButton.setOnClickListener { safeNavigate( resId = R.id.scheduleSendBottomSheetDialog, args = ScheduleSendBottomSheetDialogArgs( @@ -773,8 +774,6 @@ class NewMessageFragment : Fragment() { sendEmail() }, onNegativeButtonClicked = { if (scheduled) newMessageViewModel.resetScheduledDate() }, - // TODO: The `onDismiss` triggers everytime the Dialog is closed, even when it's not actually dismissed by the user - // onDismiss = { if (scheduled) newMessageViewModel.resetScheduledDate() }, ) } else { sendEmail() 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 0568e59696..20716938b8 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 @@ -857,10 +857,10 @@ class NewMessageViewModel @Inject constructor( }.onFailure(Sentry::captureException) } - fun setScheduleDate(date: Date) = viewModelScope.launch(ioDispatcher) { + fun setScheduleDate(date: Date?) = viewModelScope.launch(ioDispatcher) { val localUuid = draftLocalUuid ?: return@launch + scheduledDraftDate = date - draftAction = DraftAction.SCHEDULE mailboxContentRealm().write { DraftController.getDraft(localUuid, realm = this)?.also { draft -> @@ -869,17 +869,7 @@ class NewMessageViewModel @Inject constructor( } } - fun resetScheduledDate() = viewModelScope.launch(ioDispatcher) { - val localUuid = draftLocalUuid ?: return@launch - scheduledDraftDate = null - draftAction = DraftAction.SAVE - - mailboxContentRealm().write { - DraftController.getDraft(localUuid, realm = this)?.also { draft -> - draft.scheduleDate = null - } - } - } + fun resetScheduledDate() = setScheduleDate(date = null) fun storeBodyAndSubject(subject: String, html: String) { globalCoroutineScope.launch(ioDispatcher) { diff --git a/app/src/main/res/layout/fragment_new_message.xml b/app/src/main/res/layout/fragment_new_message.xml index 56058f0a28..0d0aaa59dc 100644 --- a/app/src/main/res/layout/fragment_new_message.xml +++ b/app/src/main/res/layout/fragment_new_message.xml @@ -675,7 +675,7 @@ app:layout_constraintTop_toTopOf="parent"> Date: Thu, 6 Feb 2025 14:59:17 +0100 Subject: [PATCH 59/70] refactor: Use already existing `target draft` concept to move scheduledDate & unscheduleUrl around --- .../infomaniak/mail/data/api/ApiRepository.kt | 4 +- .../data/models/draft/ScheduleDraftResult.kt | 2 +- .../mail/data/models/message/Message.kt | 2 +- .../com/infomaniak/mail/ui/MainActivity.kt | 18 ++++---- .../com/infomaniak/mail/ui/MainViewModel.kt | 8 ++-- .../mail/ui/main/thread/ThreadFragment.kt | 6 +-- .../mail/ui/newMessage/NewMessageActivity.kt | 5 +-- .../mail/ui/newMessage/NewMessageViewModel.kt | 6 +-- .../mail/workers/DraftsActionsWorker.kt | 41 +++++++++---------- 9 files changed, 44 insertions(+), 48 deletions(-) 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 a7e832c829..ac7ae5cebe 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 @@ -246,8 +246,8 @@ object ApiRepository : ApiRepositoryCore() { return callApi(ApiRoutes.draft(mailboxUuid, remoteDraftUuid), DELETE) } - fun unscheduleDraft(scheduleAction: String): ApiResponse { - return callApi(ApiRoutes.resource(scheduleAction), DELETE) + fun unscheduleDraft(unscheduleDraftUrl: String): ApiResponse { + return callApi(ApiRoutes.resource(unscheduleDraftUrl), DELETE) } fun rescheduleDraft(draftResource: String, scheduleDate: Date): ApiResponse { diff --git a/app/src/main/java/com/infomaniak/mail/data/models/draft/ScheduleDraftResult.kt b/app/src/main/java/com/infomaniak/mail/data/models/draft/ScheduleDraftResult.kt index c976b4236c..f9747e2c5a 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/draft/ScheduleDraftResult.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/draft/ScheduleDraftResult.kt @@ -24,5 +24,5 @@ import kotlinx.serialization.Serializable @Serializable data class ScheduleDraftResult( @SerialName("schedule_action") - val scheduleAction: String, + val unscheduleDraftUrl: 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 80273f4d99..965b1a1cf8 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 @@ -105,7 +105,7 @@ class Message : RealmObject { var hasUnsubscribeLink: Boolean? = null var bimi: Bimi? = null @SerialName("schedule_action") - var scheduleAction: String? = null + var unscheduleDraftUrl: String? = null // TODO: Those are unused for now, but if we ever want to use them, we need to save them in `Message.keepHeavyData()`. // If we don't do it now, we'll probably forget to do it in the future. 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 5136d53bbf..32641f79a8 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt @@ -71,7 +71,6 @@ import com.infomaniak.mail.utils.MailDateFormatUtils.mostDetailedDate import com.infomaniak.mail.utils.UiUtils.progressivelyColorSystemBars import com.infomaniak.mail.utils.Utils.Shortcuts import com.infomaniak.mail.utils.Utils.openShortcutHelp -import com.infomaniak.mail.utils.extensions.getLongOrNull import com.infomaniak.mail.utils.extensions.isUserAlreadySynchronized import com.infomaniak.mail.workers.DraftsActionsWorker import dagger.hilt.android.AndroidEntryPoint @@ -79,8 +78,10 @@ import io.sentry.Sentry import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date +import java.util.Locale import java.util.UUID import javax.inject.Inject import com.infomaniak.lib.core.R as RCore @@ -289,10 +290,13 @@ class MainActivity : BaseActivity() { showSentDraftSnackbar() } DraftAction.SCHEDULE -> { - val scheduleDate = getLongOrNull(DraftsActionsWorker.SCHEDULE_DATE_KEY) - val scheduleAction = getString(DraftsActionsWorker.SCHEDULE_ACTION_KEY) - if (scheduleDate != null && scheduleAction != null) { - showScheduledDraftSnackbar(scheduleDate = Date(scheduleDate), scheduleAction = scheduleAction) + val scheduleDate = getString(DraftsActionsWorker.SCHEDULED_DRAFT_DATE_KEY) + val unscheduleDraftUrl = getString(DraftsActionsWorker.UNSCHEDULE_DRAFT_URL_KEY) + if (scheduleDate != null && unscheduleDraftUrl != null) { + showScheduledDraftSnackbar( + scheduleDate = SimpleDateFormat(FORMAT_SCHEDULE_MAIL, Locale.getDefault()).parse(scheduleDate)!!, + unscheduleDraftUrl = unscheduleDraftUrl, + ) } } } @@ -326,7 +330,7 @@ class MainActivity : BaseActivity() { } // Still display the Snackbar even if it took three times 10 seconds of timeout to succeed - private fun showScheduledDraftSnackbar(scheduleDate: Date, scheduleAction: String) { + private fun showScheduledDraftSnackbar(scheduleDate: Date, unscheduleDraftUrl: String) { showSendingSnackbarTimer.cancel() val dateString = mostDetailedDate( @@ -338,7 +342,7 @@ class MainActivity : BaseActivity() { snackbarManager.setValue( title = String.format(getString(R.string.snackbarScheduleSaved), dateString), buttonTitle = RCore.string.buttonCancel, - customBehavior = { mainViewModel.unscheduleDraft(scheduleAction) }, + customBehavior = { mainViewModel.unscheduleDraft(unscheduleDraftUrl) }, ) } 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 fc05016380..98169176d0 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt @@ -621,11 +621,11 @@ class MainViewModel @Inject constructor( } fun modifyScheduledDraft( - scheduleAction: String, + unscheduleDraftUrl: String, onSuccess: () -> Unit, ) = viewModelScope.launch(ioCoroutineContext) { val mailbox = currentMailbox.value!! - val apiResponse = ApiRepository.unscheduleDraft(scheduleAction) + val apiResponse = ApiRepository.unscheduleDraft(unscheduleDraftUrl) if (apiResponse.isSuccess()) { val scheduledDraftsFolderId = folderController.getFolder(FolderRole.SCHEDULED_DRAFTS)!!.id @@ -636,9 +636,9 @@ class MainViewModel @Inject constructor( } } - fun unscheduleDraft(scheduleAction: String) = viewModelScope.launch(ioCoroutineContext) { + fun unscheduleDraft(unscheduleDraftUrl: String) = viewModelScope.launch(ioCoroutineContext) { val mailbox = currentMailbox.value!! - val apiResponse = ApiRepository.unscheduleDraft(scheduleAction) + val apiResponse = ApiRepository.unscheduleDraft(unscheduleDraftUrl) if (apiResponse.isSuccess()) { val scheduledDraftsFolderId = folderController.getFolder(FolderRole.SCHEDULED_DRAFTS)!!.id diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt index d6820d6f91..daa63889e0 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt @@ -756,12 +756,12 @@ class ThreadFragment : Fragment() { title = getString(R.string.editSendTitle), description = getString(R.string.editSendDescription), onPositiveButtonClicked = { - val scheduleAction = message.scheduleAction + val unscheduleDraftUrl = message.unscheduleDraftUrl val draftResource = message.draftResource - if (scheduleAction != null && draftResource != null) { + if (unscheduleDraftUrl != null && draftResource != null) { mainViewModel.modifyScheduledDraft( - scheduleAction = scheduleAction, + unscheduleDraftUrl = unscheduleDraftUrl, onSuccess = { trackNewMessageEvent(OPEN_FROM_DRAFT_NAME) twoPaneViewModel.navigateToNewMessage( diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt index 04e92211a4..e6f756f21f 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageActivity.kt @@ -135,10 +135,7 @@ class NewMessageActivity : BaseActivity() { } private fun startWorker() { - draftsActionsWorkerScheduler.scheduleWork( - draftLocalUuid = newMessageViewModel.draftLocalUuid(), - scheduleDate = newMessageViewModel.scheduledDraftDate, - ) + draftsActionsWorkerScheduler.scheduleWork(newMessageViewModel.draftLocalUuid()) } data class DraftSaveConfiguration( 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 20716938b8..7f4c39edbb 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 @@ -137,7 +137,6 @@ class NewMessageViewModel @Inject constructor( var isExternalBannerManuallyClosed = false var draftAction = DraftAction.SAVE var signaturesCount = 0 - var scheduledDraftDate: Date? = null private var isNewMessage = false private var snapshot: DraftSnapshot? = null @@ -859,12 +858,9 @@ class NewMessageViewModel @Inject constructor( fun setScheduleDate(date: Date?) = viewModelScope.launch(ioDispatcher) { val localUuid = draftLocalUuid ?: return@launch - - scheduledDraftDate = date - mailboxContentRealm().write { DraftController.getDraft(localUuid, realm = this)?.also { draft -> - draft.scheduleDate = scheduledDraftDate?.format(FORMAT_SCHEDULE_MAIL) + draft.scheduleDate = date?.format(FORMAT_SCHEDULE_MAIL) } } } 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 dbee5cf663..f2717b822f 100644 --- a/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt +++ b/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt @@ -47,7 +47,6 @@ import com.infomaniak.mail.utils.* import com.infomaniak.mail.utils.LocalStorageUtils.deleteDraftUploadDir import com.infomaniak.mail.utils.SharedUtils.Companion.updateSignatures import com.infomaniak.mail.utils.WorkerUtils.UploadMissingLocalFileException -import com.infomaniak.mail.utils.extensions.getLongOrNull import com.infomaniak.mail.utils.extensions.throwErrorAsException import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -90,11 +89,6 @@ class DraftsActionsWorker @AssistedInject constructor( private lateinit var userApiToken: String private var isSnackbarFeedbackNeeded: Boolean = false - //region Scheduled Drafts - private var scheduledDraftDate: Long? = null - private var scheduledDraftAction: String? = null - //endregion - private val dateFormatWithTimezone by lazy { SimpleDateFormat(FORMAT_DATE_WITH_TIMEZONE, Locale.ROOT) } override suspend fun launchWork(): Result = withContext(ioDispatcher) { @@ -107,9 +101,6 @@ class DraftsActionsWorker @AssistedInject constructor( mailboxId = inputData.getIntOrNull(MAILBOX_ID_KEY) ?: return@withContext Result.failure() draftLocalUuid = inputData.getString(DRAFT_LOCAL_UUID_KEY) - scheduledDraftDate = inputData.getLongOrNull(SCHEDULE_DATE_KEY) - scheduledDraftAction = inputData.getString(SCHEDULE_ACTION_KEY) - userApiToken = AccountUtils.getUserById(userId)?.apiToken?.accessToken ?: return@withContext Result.failure() mailbox = mailboxController.getMailbox(userId, mailboxId) ?: return@withContext Result.failure() okHttpClient = AccountUtils.getHttpClient(userId) @@ -143,6 +134,8 @@ class DraftsActionsWorker @AssistedInject constructor( var trackedDraftErrorMessageResId: Int? = null var remoteUuidOfTrackedDraft: String? = null var trackedDraftAction: DraftAction? = null + var trackedScheduledDraftDate: String? = null + var trackedUnscheduledDraftUrl: String? = null var isTrackedDraftSuccess: Boolean? = null val drafts = draftController.getDraftsWithActions(mailboxContentRealm) @@ -154,7 +147,10 @@ class DraftsActionsWorker @AssistedInject constructor( drafts.asReversed().forEach { draft -> val isTargetDraft = draft.localUuid == draftLocalUuid - if (isTargetDraft) trackedDraftAction = draft.action + if (isTargetDraft) { + trackedDraftAction = draft.action + trackedScheduledDraftDate = draft.scheduleDate + } runCatching { val updatedDraft = uploadAttachmentsWithMutex(draft, mailbox, draftController, mailboxContentRealm) @@ -162,12 +158,12 @@ class DraftsActionsWorker @AssistedInject constructor( if (isSuccess) { if (isTargetDraft) { remoteUuidOfTrackedDraft = savedDraftUuid + trackedUnscheduledDraftUrl = unscheduleDraftUrl isTrackedDraftSuccess = true } scheduledMessageEtop?.let(scheduledMessageEtops::add) realmActionOnDraft?.let(realmActionsOnDraft::add) - scheduledDraftAction = scheduleAction } else if (isTargetDraft) { trackedDraftErrorMessageResId = errorMessageResId!! isTrackedDraftSuccess = false @@ -227,6 +223,8 @@ class DraftsActionsWorker @AssistedInject constructor( remoteUuidOfTrackedDraft, trackedDraftAction, trackedDraftErrorMessageResId, + trackedScheduledDraftDate, + trackedUnscheduledDraftUrl, ) } @@ -268,6 +266,8 @@ class DraftsActionsWorker @AssistedInject constructor( remoteUuidOfTrackedDraft: String?, trackedDraftAction: DraftAction?, trackedDraftErrorMessageResId: Int?, + trackedScheduledDraftDate: String?, + trackedUnscheduleDraftUrl: String?, ): Result { val biggestScheduleMessageEtop = scheduledMessagesEtops.mapNotNull { dateFormatWithTimezone.parse(it)?.time }.maxOrNull() @@ -280,8 +280,8 @@ class DraftsActionsWorker @AssistedInject constructor( RESULT_DRAFT_ACTION_KEY to draftLocalUuid?.let { trackedDraftAction?.name }, BIGGEST_SCHEDULED_MESSAGE_ETOP_KEY to biggestScheduleMessageEtop, RESULT_USER_ID_KEY to userId, - SCHEDULE_DATE_KEY to scheduledDraftDate, - SCHEDULE_ACTION_KEY to scheduledDraftAction, + SCHEDULED_DRAFT_DATE_KEY to trackedScheduledDraftDate, + UNSCHEDULE_DRAFT_URL_KEY to trackedUnscheduleDraftUrl, ) } else { Data.EMPTY @@ -304,7 +304,7 @@ class DraftsActionsWorker @AssistedInject constructor( data class DraftActionResult( val realmActionOnDraft: ((MutableRealm) -> Unit)?, val scheduledMessageEtop: String?, - val scheduleAction: String?, + val unscheduleDraftUrl: String?, val errorMessageResId: Int?, val savedDraftUuid: String?, val isSuccess: Boolean, @@ -334,7 +334,7 @@ class DraftsActionsWorker @AssistedInject constructor( return DraftActionResult( realmActionOnDraft = null, scheduledMessageEtop = null, - scheduleAction = null, + unscheduleDraftUrl = null, errorMessageResId = R.string.errorCorruptAttachment, savedDraftUuid = null, isSuccess = false, @@ -380,7 +380,7 @@ class DraftsActionsWorker @AssistedInject constructor( suspend fun executeScheduleAction() = with(ApiRepository.scheduleDraft(mailboxUuid, draft, okHttpClient)) { when { isSuccess() -> { - scheduleDraftAction = data?.scheduleAction + scheduleDraftAction = data?.unscheduleDraftUrl realmActionOnDraft = deleteDraftCallback(draft) refreshScheduledDraftsFolder() } @@ -408,7 +408,7 @@ class DraftsActionsWorker @AssistedInject constructor( return DraftActionResult( realmActionOnDraft = realmActionOnDraft, scheduledMessageEtop = scheduledMessageEtop, - scheduleAction = scheduleDraftAction, + unscheduleDraftUrl = scheduleDraftAction, errorMessageResId = null, savedDraftUuid = savedDraftUuid, isSuccess = true, @@ -456,7 +456,7 @@ class DraftsActionsWorker @AssistedInject constructor( private val workManager: WorkManager, ) { - fun scheduleWork(draftLocalUuid: String? = null, scheduleDate: Date? = null) { + fun scheduleWork(draftLocalUuid: String? = null) { if (AccountUtils.currentMailboxId == AppSettings.DEFAULT_ID) return if (draftController.getDraftsWithActionsCount() == 0L) return @@ -467,7 +467,6 @@ class DraftsActionsWorker @AssistedInject constructor( USER_ID_KEY to AccountUtils.currentUserId, MAILBOX_ID_KEY to AccountUtils.currentMailboxId, DRAFT_LOCAL_UUID_KEY to draftLocalUuid, - SCHEDULE_DATE_KEY to scheduleDate?.time, ) val workRequest = OneTimeWorkRequestBuilder() .addTag(TAG) @@ -503,7 +502,7 @@ class DraftsActionsWorker @AssistedInject constructor( const val RESULT_DRAFT_ACTION_KEY = "resultDraftActionKey" const val BIGGEST_SCHEDULED_MESSAGE_ETOP_KEY = "biggestScheduledMessageEtopKey" const val RESULT_USER_ID_KEY = "resultUserIdKey" - const val SCHEDULE_DATE_KEY = "scheduleDateKey" - const val SCHEDULE_ACTION_KEY = "scheduleActionKey" + const val SCHEDULED_DRAFT_DATE_KEY = "scheduledDraftDateKey" + const val UNSCHEDULE_DRAFT_URL_KEY = "unscheduleDraftUrlKey" } } From ed379c2055bac776155bc9cee34382403d836051 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 7 Feb 2025 07:06:05 +0100 Subject: [PATCH 60/70] fix: Also filter on limited for ScheduleSend paywall --- .../java/com/infomaniak/mail/data/models/mailbox/Mailbox.kt | 2 ++ .../com/infomaniak/mail/ui/main/thread/ThreadFragment.kt | 4 ++-- .../com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/models/mailbox/Mailbox.kt b/app/src/main/java/com/infomaniak/mail/data/models/mailbox/Mailbox.kt index 6ec9a4c8c5..2ef138894d 100644 --- a/app/src/main/java/com/infomaniak/mail/data/models/mailbox/Mailbox.kt +++ b/app/src/main/java/com/infomaniak/mail/data/models/mailbox/Mailbox.kt @@ -112,6 +112,8 @@ class Mailbox : RealmObject { shouldDisplayPastille = unreadCountLocal == 0 && unreadCountRemote > 0, ) + val isFreeMailbox: Boolean get() = isFree && isLimited + private fun createObjectId(userId: Int): String = "${userId}_${this.mailboxId}" fun initLocalValues( diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt index daa63889e0..4727e6b44f 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt @@ -544,7 +544,7 @@ class ThreadFragment : Fragment() { isAlreadyScheduled = false, draftResource = mainViewModel.draftResource, lastSelectedScheduleEpoch = localSettings.lastSelectedScheduleEpoch ?: 0L, - isCurrentMailboxFree = mainViewModel.currentMailbox.value?.isFree ?: true, + isCurrentMailboxFree = mainViewModel.currentMailbox.value?.isFreeMailbox ?: true, ).toBundle(), ) } @@ -745,7 +745,7 @@ class ThreadFragment : Fragment() { isAlreadyScheduled = true, draftResource = draftResource, lastSelectedScheduleEpoch = localSettings.lastSelectedScheduleEpoch ?: 0L, - isCurrentMailboxFree = mainViewModel.currentMailbox.value?.isFree ?: true, + isCurrentMailboxFree = mainViewModel.currentMailbox.value?.isFreeMailbox ?: true, ).toBundle(), currentClassName = ThreadFragment::class.java.name, ) diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 07650c60cd..fbf01e3278 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -223,7 +223,7 @@ class NewMessageFragment : Fragment() { args = ScheduleSendBottomSheetDialogArgs( isAlreadyScheduled = false, lastSelectedScheduleEpoch = localSettings.lastSelectedScheduleEpoch ?: 0L, - isCurrentMailboxFree = newMessageViewModel.currentMailbox.isFree, + isCurrentMailboxFree = newMessageViewModel.currentMailbox.isFreeMailbox, ).toBundle(), ) } @@ -738,10 +738,11 @@ class NewMessageFragment : Fragment() { args = ScheduleSendBottomSheetDialogArgs( isAlreadyScheduled = false, lastSelectedScheduleEpoch = localSettings.lastSelectedScheduleEpoch ?: 0L, - isCurrentMailboxFree = newMessageViewModel.currentMailbox.isFree, + isCurrentMailboxFree = newMessageViewModel.currentMailbox.isFreeMailbox, ).toBundle(), ) } + sendButton.setOnClickListener { tryToSendEmail() } } From 2c094f19ea9aa97c224825e33493aedb2b97c8c9 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 7 Feb 2025 08:29:24 +0100 Subject: [PATCH 61/70] fix: Correctly refresh Folders after scheduling a mail --- .../com/infomaniak/mail/ui/MainActivity.kt | 2 +- .../mail/workers/DraftsActionsWorker.kt | 36 +++++++------------ 2 files changed, 13 insertions(+), 25 deletions(-) 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 32641f79a8..9272dc35d1 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt @@ -307,7 +307,7 @@ class MainActivity : BaseActivity() { val userId = getInt(DraftsActionsWorker.RESULT_USER_ID_KEY, 0) if (userId != AccountUtils.currentUserId) return - getLong(DraftsActionsWorker.BIGGEST_SCHEDULED_MESSAGE_ETOP_KEY, 0).takeIf { it > 0 }?.let { scheduledMessageEtop -> + getLong(DraftsActionsWorker.BIGGEST_SCHEDULED_MESSAGES_ETOP_KEY, 0).takeIf { it > 0 }?.let { scheduledMessageEtop -> mainViewModel.refreshDraftFolderWhenDraftArrives(scheduledMessageEtop) } } 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 f2717b822f..4164b0ddab 100644 --- a/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt +++ b/app/src/main/java/com/infomaniak/mail/workers/DraftsActionsWorker.kt @@ -34,11 +34,9 @@ import com.infomaniak.mail.data.cache.RealmDatabase import com.infomaniak.mail.data.cache.mailboxContent.DraftController import com.infomaniak.mail.data.cache.mailboxContent.FolderController import com.infomaniak.mail.data.cache.mailboxContent.RefreshController -import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RefreshMode import com.infomaniak.mail.data.cache.mailboxInfo.MailboxController import com.infomaniak.mail.data.models.AppSettings import com.infomaniak.mail.data.models.AttachmentUploadStatus -import com.infomaniak.mail.data.models.Folder.FolderRole import com.infomaniak.mail.data.models.draft.Draft import com.infomaniak.mail.data.models.draft.Draft.DraftAction import com.infomaniak.mail.data.models.mailbox.Mailbox @@ -130,7 +128,7 @@ class DraftsActionsWorker @AssistedInject constructor( // List containing the callback function to update/delete drafts in Realm // We keep these Realm changes in a List to execute them all in a unique Realm transaction at the end of the worker val realmActionsOnDraft = mutableListOf<(MutableRealm) -> Unit>() - val scheduledMessageEtops = mutableListOf() + val scheduledMessagesEtops = mutableListOf() var trackedDraftErrorMessageResId: Int? = null var remoteUuidOfTrackedDraft: String? = null var trackedDraftAction: DraftAction? = null @@ -161,7 +159,7 @@ class DraftsActionsWorker @AssistedInject constructor( trackedUnscheduledDraftUrl = unscheduleDraftUrl isTrackedDraftSuccess = true } - scheduledMessageEtop?.let(scheduledMessageEtops::add) + scheduledMessageEtop?.let(scheduledMessagesEtops::add) realmActionOnDraft?.let(realmActionsOnDraft::add) } else if (isTargetDraft) { @@ -217,7 +215,7 @@ class DraftsActionsWorker @AssistedInject constructor( showDraftErrorNotification(isTrackedDraftSuccess, trackedDraftErrorMessageResId, trackedDraftAction) return computeResult( - scheduledMessageEtops, + scheduledMessagesEtops, haveAllDraftsSucceeded, isTrackedDraftSuccess, remoteUuidOfTrackedDraft, @@ -247,18 +245,6 @@ class DraftsActionsWorker @AssistedInject constructor( } } - private suspend fun refreshScheduledDraftsFolder() { - val currentMailbox = mailboxController.getMailbox(AccountUtils.currentUserId, AccountUtils.currentMailboxId) ?: return - val folderId = folderController.getFolder(FolderRole.SCHEDULED_DRAFTS)?.id ?: return - - refreshController.refreshThreads( - refreshMode = RefreshMode.REFRESH_FOLDER_WITH_ROLE, - mailbox = currentMailbox, - folderId = folderId, - realm = mailboxContentRealm, - ) - } - private fun computeResult( scheduledMessagesEtops: MutableList, haveAllDraftsSucceeded: Boolean, @@ -270,7 +256,9 @@ class DraftsActionsWorker @AssistedInject constructor( trackedUnscheduleDraftUrl: String?, ): Result { - val biggestScheduleMessageEtop = scheduledMessagesEtops.mapNotNull { dateFormatWithTimezone.parse(it)?.time }.maxOrNull() + val biggestScheduledMessagesEtop = scheduledMessagesEtops.mapNotNull { + dateFormatWithTimezone.parse(it)?.time + }.maxOrNull() return if (haveAllDraftsSucceeded || isTrackedDraftSuccess == true) { val outputData = if (isSnackbarFeedbackNeeded) { @@ -278,7 +266,7 @@ class DraftsActionsWorker @AssistedInject constructor( REMOTE_DRAFT_UUID_KEY to draftLocalUuid?.let { remoteUuidOfTrackedDraft }, ASSOCIATED_MAILBOX_UUID_KEY to draftLocalUuid?.let { mailbox.uuid }, RESULT_DRAFT_ACTION_KEY to draftLocalUuid?.let { trackedDraftAction?.name }, - BIGGEST_SCHEDULED_MESSAGE_ETOP_KEY to biggestScheduleMessageEtop, + BIGGEST_SCHEDULED_MESSAGES_ETOP_KEY to biggestScheduledMessagesEtop, RESULT_USER_ID_KEY to userId, SCHEDULED_DRAFT_DATE_KEY to trackedScheduledDraftDate, UNSCHEDULE_DRAFT_URL_KEY to trackedUnscheduleDraftUrl, @@ -291,7 +279,7 @@ class DraftsActionsWorker @AssistedInject constructor( val outputData = if (isSnackbarFeedbackNeeded) { workDataOf( ERROR_MESSAGE_RESID_KEY to trackedDraftErrorMessageResId, - BIGGEST_SCHEDULED_MESSAGE_ETOP_KEY to biggestScheduleMessageEtop, + BIGGEST_SCHEDULED_MESSAGES_ETOP_KEY to biggestScheduledMessagesEtop, RESULT_USER_ID_KEY to userId, ) } else { @@ -360,8 +348,8 @@ class DraftsActionsWorker @AssistedInject constructor( suspend fun executeSendAction() = with(ApiRepository.sendDraft(mailboxUuid, draft, okHttpClient)) { when { isSuccess() -> { - scheduledMessageEtop = data?.scheduledMessageEtop realmActionOnDraft = deleteDraftCallback(draft) + scheduledMessageEtop = data?.scheduledMessageEtop } error?.exception is SerializationException -> { realmActionOnDraft = deleteDraftCallback(draft) @@ -380,9 +368,9 @@ class DraftsActionsWorker @AssistedInject constructor( suspend fun executeScheduleAction() = with(ApiRepository.scheduleDraft(mailboxUuid, draft, okHttpClient)) { when { isSuccess() -> { - scheduleDraftAction = data?.unscheduleDraftUrl realmActionOnDraft = deleteDraftCallback(draft) - refreshScheduledDraftsFolder() + scheduledMessageEtop = dateFormatWithTimezone.format(Date()) + scheduleDraftAction = data?.unscheduleDraftUrl } error?.exception is SerializationException -> { realmActionOnDraft = deleteDraftCallback(draft) @@ -500,7 +488,7 @@ class DraftsActionsWorker @AssistedInject constructor( const val REMOTE_DRAFT_UUID_KEY = "remoteDraftUuidKey" const val ASSOCIATED_MAILBOX_UUID_KEY = "associatedMailboxUuidKey" const val RESULT_DRAFT_ACTION_KEY = "resultDraftActionKey" - const val BIGGEST_SCHEDULED_MESSAGE_ETOP_KEY = "biggestScheduledMessageEtopKey" + const val BIGGEST_SCHEDULED_MESSAGES_ETOP_KEY = "biggestScheduledMessagesEtopKey" const val RESULT_USER_ID_KEY = "resultUserIdKey" const val SCHEDULED_DRAFT_DATE_KEY = "scheduledDraftDateKey" const val UNSCHEDULE_DRAFT_URL_KEY = "unscheduleDraftUrlKey" From b267d1be5463d34835c0cb7004d84da621b16310 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 7 Feb 2025 08:59:48 +0100 Subject: [PATCH 62/70] refactor: Update german string --- app/src/main/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index eb7fb9489a..dc83a65a04 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -159,7 +159,7 @@ Neu planen E-Mails wiederherstellen Planen Sie den Versand von E-Mails für einen späteren Zeitpunkt - Programmieren + Einplanen Siehe Alle Teilen Sie From fbd1cc0302871457dcbf328cf8de6c6cff53b18e Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 7 Feb 2025 09:51:55 +0100 Subject: [PATCH 63/70] refactor: Normalize SelectDateAndTimeForScheduledDraftDialog callbacks --- ...electDateAndTimeForScheduledDraftDialog.kt | 50 +++++-------------- .../mail/ui/main/thread/ThreadFragment.kt | 25 +++------- .../mail/ui/newMessage/NewMessageFragment.kt | 5 +- 3 files changed, 22 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt index 44979ae2f1..7d4898403e 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt @@ -46,10 +46,8 @@ open class SelectDateAndTimeForScheduledDraftDialog @Inject constructor( override val alertDialog = initDialog() - private var onPositiveButtonClicked: (() -> Unit)? = null - private var onNegativeButtonClicked: (() -> Unit)? = null - private var onDismissed: (() -> Unit)? = null - private var onCancelled: (() -> Unit)? = null + private var onSchedule: (() -> Unit)? = null + private var onAbort: (() -> Unit)? = null lateinit var selectedDate: Date @@ -75,21 +73,13 @@ open class SelectDateAndTimeForScheduledDraftDialog @Inject constructor( } final override fun resetCallbacks() { - onPositiveButtonClicked = null - onNegativeButtonClicked = null - onDismissed = null - onCancelled = null + onSchedule = null + onAbort = null } - fun show( - title: String, - onPositiveButtonClicked: () -> Unit, - onNegativeButtonClicked: (() -> Unit)? = null, - onDismiss: (() -> Unit)? = null, - onCancel: (() -> Unit)? = null, - ) { + fun show(title: String, onSchedule: () -> Unit, onAbort: (() -> Unit)? = null) { showDialogWithBasicInfo(title, R.string.buttonScheduleTitle) - setupListeners(onPositiveButtonClicked, onNegativeButtonClicked, onDismiss, onCancel) + setupListeners(onSchedule, onAbort) } private fun getScheduleDateErrorText(): String = if (selectedDate.isInTheFuture().not()) { @@ -102,12 +92,7 @@ open class SelectDateAndTimeForScheduledDraftDialog @Inject constructor( ) } - private fun setupListeners( - onPositiveButtonClicked: () -> Unit, - onNegativeButtonClicked: (() -> Unit)?, - onDismiss: (() -> Unit)?, - onCancel: (() -> Unit)?, - ) = with(alertDialog) { + private fun setupListeners(onSchedule: () -> Unit, onAbort: (() -> Unit)?) = with(alertDialog) { binding.dateField.setOnClickListener { datePicker?.show(super.activity.supportFragmentManager, "tag") } @@ -136,28 +121,17 @@ open class SelectDateAndTimeForScheduledDraftDialog @Inject constructor( positiveButton.isEnabled = selectedDate.isAtLeastXMinutesInTheFuture(MIN_SCHEDULE_DELAY_MINUTES) } - this@SelectDateAndTimeForScheduledDraftDialog.onPositiveButtonClicked = onPositiveButtonClicked - this@SelectDateAndTimeForScheduledDraftDialog.onNegativeButtonClicked = onNegativeButtonClicked + this@SelectDateAndTimeForScheduledDraftDialog.onSchedule = onSchedule + this@SelectDateAndTimeForScheduledDraftDialog.onAbort = onAbort positiveButton.setOnClickListener { - this@SelectDateAndTimeForScheduledDraftDialog.onPositiveButtonClicked?.invoke() + this@SelectDateAndTimeForScheduledDraftDialog.onSchedule?.invoke() dismiss() } - negativeButton.setOnClickListener { - this@SelectDateAndTimeForScheduledDraftDialog.onNegativeButtonClicked?.invoke() - cancel() - } - - onDismiss.let { - onDismissed = it - setOnDismissListener { onDismissed?.invoke() } - } + negativeButton.setOnClickListener { cancel() } - onCancel?.let { - onCancelled = it - setOnCancelListener { onCancelled?.invoke() } - } + setOnCancelListener { onAbort?.invoke() } } private fun setTimePicker() = with(binding) { diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt index 4727e6b44f..155fe3b9a4 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt @@ -537,27 +537,14 @@ class ThreadFragment : Fragment() { private fun setupBackActionHandler() { - fun navigateBackToBottomSheet() { - safeNavigate( - resId = R.id.scheduleSendBottomSheetDialog, - args = ScheduleSendBottomSheetDialogArgs( - isAlreadyScheduled = false, - draftResource = mainViewModel.draftResource, - lastSelectedScheduleEpoch = localSettings.lastSelectedScheduleEpoch ?: 0L, - isCurrentMailboxFree = mainViewModel.currentMailbox.value?.isFreeMailbox ?: true, - ).toBundle(), - ) - } - getBackNavigationResult(OPEN_DATE_AND_TIME_SCHEDULE_DIALOG) { _: Boolean -> dateAndTimeScheduleDialog.show( title = getString(R.string.datePickerTitle), - onPositiveButtonClicked = { + onSchedule = { val scheduleDate = dateAndTimeScheduleDialog.selectedDate.time localSettings.lastSelectedScheduleEpoch = scheduleDate }, - onNegativeButtonClicked = ::navigateBackToBottomSheet, - onCancel = ::navigateBackToBottomSheet, + onAbort = { navigateToScheduleSendBottomSheet(isAlreadyScheduled = false) }, ) } @@ -739,11 +726,15 @@ class ThreadFragment : Fragment() { private fun rescheduleDraft(draftResource: String) { mainViewModel.draftResource = draftResource + navigateToScheduleSendBottomSheet(isAlreadyScheduled = true) + } + + private fun navigateToScheduleSendBottomSheet(isAlreadyScheduled: Boolean) { safeNavigate( resId = R.id.scheduleSendBottomSheetDialog, args = ScheduleSendBottomSheetDialogArgs( - isAlreadyScheduled = true, - draftResource = draftResource, + isAlreadyScheduled = isAlreadyScheduled, + draftResource = mainViewModel.draftResource, lastSelectedScheduleEpoch = localSettings.lastSelectedScheduleEpoch ?: 0L, isCurrentMailboxFree = mainViewModel.currentMailbox.value?.isFreeMailbox ?: true, ).toBundle(), diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index fbf01e3278..7516f22183 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -236,13 +236,12 @@ class NewMessageFragment : Fragment() { getBackNavigationResult(OPEN_DATE_AND_TIME_SCHEDULE_DIALOG) { _: Boolean -> dateAndTimeScheduleDialog.show( title = getString(R.string.datePickerTitle), - onPositiveButtonClicked = { + onSchedule = { val scheduleDate = dateAndTimeScheduleDialog.selectedDate.time localSettings.lastSelectedScheduleEpoch = scheduleDate scheduleDraft(scheduleDate) }, - onNegativeButtonClicked = ::navigateBackToBottomSheet, - onCancel = ::navigateBackToBottomSheet, + onAbort = ::navigateBackToBottomSheet, ) } From e50ac558a3f5caea21a6aa10da117020b984ae3b Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 7 Feb 2025 09:52:20 +0100 Subject: [PATCH 64/70] fix: Remove now useless ActionsBottomSheetDialog dependency in ScheduleSendBottomSheetDialog --- .../bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt index 0cc83213a7..5ae303d0f4 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt @@ -25,16 +25,14 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.view.children import androidx.core.view.isVisible -import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.navArgs +import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.infomaniak.lib.core.utils.* import com.infomaniak.mail.MatomoMail.trackScheduleSendEvent import com.infomaniak.mail.R import com.infomaniak.mail.databinding.BottomSheetScheduleSendBinding -import com.infomaniak.mail.ui.MainViewModel import com.infomaniak.mail.ui.alertDialogs.SelectDateAndTimeForScheduledDraftDialog.Companion.MIN_SCHEDULE_DELAY_MINUTES import com.infomaniak.mail.ui.main.thread.actions.ActionItemView -import com.infomaniak.mail.ui.main.thread.actions.ActionsBottomSheetDialog import com.infomaniak.mail.utils.MailDateFormatUtils.mostDetailedDate import dagger.hilt.android.AndroidEntryPoint import java.util.Date @@ -42,12 +40,10 @@ import javax.inject.Inject @AndroidEntryPoint -class ScheduleSendBottomSheetDialog @Inject constructor() : ActionsBottomSheetDialog() { +class ScheduleSendBottomSheetDialog @Inject constructor() : BottomSheetDialogFragment() { private val navigationArgs: ScheduleSendBottomSheetDialogArgs by navArgs() - private var binding: BottomSheetScheduleSendBinding by safeBinding() - override val mainViewModel: MainViewModel by activityViewModels() // Navigation args does not support nullable primitive types, so we use 0L // as a replacement (corresponding to Thursday 1 January 1970 00:00:00 UT). From 635fe0d42cd541ccae75652d44d0ddce8568df4c Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 7 Feb 2025 10:36:11 +0100 Subject: [PATCH 65/70] refactor: Normalize DescriptionAlertDialog callbacks --- .../ui/alertDialogs/DescriptionAlertDialog.kt | 26 +++++++++---------- .../mail/ui/main/folder/ThreadListFragment.kt | 2 +- .../threadMode/ThreadModeSettingFragment.kt | 2 +- .../mail/ui/newMessage/NewMessageFragment.kt | 2 +- .../mail/utils/extensions/Extensions.kt | 4 +-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/DescriptionAlertDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/DescriptionAlertDialog.kt index b5eec1eb72..f23fb325c1 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/DescriptionAlertDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/DescriptionAlertDialog.kt @@ -41,7 +41,7 @@ open class DescriptionAlertDialog @Inject constructor( private var onPositiveButtonClicked: (() -> Unit)? = null private var onNegativeButtonClicked: (() -> Unit)? = null - private var onDismissed: (() -> Unit)? = null + private var onCancelled: (() -> Unit)? = null protected fun initDialog(customThemeRes: Int? = null) = with(binding) { val builder = customThemeRes?.let { MaterialAlertDialogBuilder(context, it) } ?: MaterialAlertDialogBuilder(context) @@ -56,7 +56,7 @@ open class DescriptionAlertDialog @Inject constructor( final override fun resetCallbacks() { onPositiveButtonClicked = null onNegativeButtonClicked = null - onDismissed = null + onCancelled = null } fun show( @@ -68,35 +68,35 @@ open class DescriptionAlertDialog @Inject constructor( @StringRes negativeButtonText: Int? = null, onPositiveButtonClicked: () -> Unit, onNegativeButtonClicked: (() -> Unit)? = null, - onDismiss: (() -> Unit)? = null, + onCancel: (() -> Unit)? = null, ) { showDialogWithBasicInfo(title, description, displayCancelButton, positiveButtonText, negativeButtonText) if (displayLoader) initProgress() - setupListeners(displayLoader, onPositiveButtonClicked, onNegativeButtonClicked, onDismiss) + setupListeners(displayLoader, onPositiveButtonClicked, onNegativeButtonClicked, onCancel) } private fun setupListeners( displayLoader: Boolean, onPositiveButtonClicked: () -> Unit, onNegativeButtonClicked: (() -> Unit)?, - onDismiss: (() -> Unit)?, + onCancel: (() -> Unit)?, ) = with(alertDialog) { this@DescriptionAlertDialog.onPositiveButtonClicked = onPositiveButtonClicked - this@DescriptionAlertDialog.onNegativeButtonClicked = onNegativeButtonClicked - positiveButton.setOnClickListener { this@DescriptionAlertDialog.onPositiveButtonClicked?.invoke() if (displayLoader) startLoading() else dismiss() } + + this@DescriptionAlertDialog.onNegativeButtonClicked = onNegativeButtonClicked negativeButton.setOnClickListener { this@DescriptionAlertDialog.onNegativeButtonClicked?.invoke() - dismiss() + cancel() } - onDismiss?.let { - onDismissed = it - setOnDismissListener { onDismissed?.invoke() } + onCancel?.let { + onCancelled = it + setOnCancelListener { onCancelled?.invoke() } } } @@ -104,7 +104,7 @@ open class DescriptionAlertDialog @Inject constructor( deletedCount: Int, displayLoader: Boolean, onPositiveButtonClicked: () -> Unit, - onDismissed: (() -> Unit)? = null, + onCancel: (() -> Unit)? = null, ) = show( title = activityContext.resources.getQuantityString( R.plurals.threadListDeletionConfirmationAlertTitle, @@ -117,7 +117,7 @@ open class DescriptionAlertDialog @Inject constructor( ), displayLoader = displayLoader, onPositiveButtonClicked = onPositiveButtonClicked, - onDismiss = onDismissed, + onCancel = onCancel, ) protected fun showDialogWithBasicInfo( diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListFragment.kt index 9843368e10..ff4b92ac89 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListFragment.kt @@ -451,7 +451,7 @@ class ThreadListFragment : TwoPaneFragment() { folderRole = folderRole, count = 1, displayLoader = false, - onDismiss = { + onCancel = { // Notify only if the user cancelled the popup (e.g. the thread is not deleted), // otherwise it will notify the next item in the list and make it slightly blink if (threadListAdapter.dataSet.indexOf(thread) == position) threadListAdapter.notifyItemChanged(position) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/threadMode/ThreadModeSettingFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/threadMode/ThreadModeSettingFragment.kt index 6efb045c54..646af328b9 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/threadMode/ThreadModeSettingFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/settings/appearance/threadMode/ThreadModeSettingFragment.kt @@ -75,7 +75,7 @@ class ThreadModeSettingFragment : Fragment() { localSettings.threadMode = threadMode threadModeSettingViewModel.dropAllMailboxesContentThenReloadApp() }, - onDismiss = { check(localSettings.threadMode) }, + onCancel = { check(localSettings.threadMode) }, ) } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 7516f22183..6e70caa9b3 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -773,7 +773,7 @@ class NewMessageFragment : Fragment() { trackNewMessageEvent("sendWithoutSubjectConfirm") sendEmail() }, - onNegativeButtonClicked = { if (scheduled) newMessageViewModel.resetScheduledDate() }, + onCancel = { if (scheduled) newMessageViewModel.resetScheduledDate() }, ) } else { sendEmail() diff --git a/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt b/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt index 4a3dfe1ac3..1862e6df2a 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt @@ -410,10 +410,10 @@ fun DescriptionAlertDialog.deleteWithConfirmationPopup( folderRole: FolderRole?, count: Int, displayLoader: Boolean = true, - onDismiss: (() -> Unit)? = null, + onCancel: (() -> Unit)? = null, callback: () -> Unit, ) = if (isPermanentDeleteFolder(folderRole) && folderRole != FolderRole.DRAFT) { // We don't want to display the popup for Drafts - showDeletePermanentlyDialog(count, displayLoader, callback, onDismiss) + showDeletePermanentlyDialog(count, displayLoader, callback, onCancel) } else { callback() } From 07944261dd9a1f621b4ffdbe147e46d3f2b913db Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 7 Feb 2025 11:36:39 +0100 Subject: [PATCH 66/70] fix: Remove now unused `isAlreadyScheduled` & `draftResource` in ScheduleSendBottomSheetDialog --- .../com/infomaniak/mail/ui/MainViewModel.kt | 23 +++++------ ...electDateAndTimeForScheduledDraftDialog.kt | 10 ++--- .../ScheduleSendBottomSheetDialog.kt | 16 ++------ .../mail/ui/main/thread/ThreadFragment.kt | 14 +++---- .../mail/ui/newMessage/NewMessageFragment.kt | 41 +++++++------------ .../main/res/navigation/main_navigation.xml | 9 ---- .../res/navigation/new_message_navigation.xml | 13 +----- 7 files changed, 41 insertions(+), 85 deletions(-) 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 98169176d0..71ba7f62a8 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt @@ -603,20 +603,17 @@ class MainViewModel @Inject constructor( //region Scheduled Drafts fun rescheduleDraft(scheduleDate: Date) = viewModelScope.launch(ioCoroutineContext) { - draftResource.let { resource -> - if (resource.isNullOrBlank()) { - snackbarManager.postValue(title = appContext.getString(RCore.string.anErrorHasOccurred)) - return@launch - } - - val apiResponse = ApiRepository.rescheduleDraft(resource, scheduleDate) - - if (apiResponse.isSuccess()) { - val scheduledDraftsFolderId = folderController.getFolder(FolderRole.SCHEDULED_DRAFTS)!!.id - refreshFoldersAsync(currentMailbox.value!!, listOf(scheduledDraftsFolderId)) - } else { - snackbarManager.postValue(title = appContext.getString(apiResponse.translatedError)) + draftResource?.takeIf { it.isNotBlank() }?.let { resource -> + with(ApiRepository.rescheduleDraft(resource, scheduleDate)) { + if (isSuccess()) { + val scheduledDraftsFolderId = folderController.getFolder(FolderRole.SCHEDULED_DRAFTS)!!.id + refreshFoldersAsync(currentMailbox.value!!, listOf(scheduledDraftsFolderId)) + } else { + snackbarManager.postValue(title = appContext.getString(translatedError)) + } } + } ?: run { + snackbarManager.postValue(title = appContext.getString(RCore.string.anErrorHasOccurred)) } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt index 7d4898403e..97498e1cc8 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/alertDialogs/SelectDateAndTimeForScheduledDraftDialog.kt @@ -46,10 +46,10 @@ open class SelectDateAndTimeForScheduledDraftDialog @Inject constructor( override val alertDialog = initDialog() - private var onSchedule: (() -> Unit)? = null + private var onSchedule: ((Long) -> Unit)? = null private var onAbort: (() -> Unit)? = null - lateinit var selectedDate: Date + private lateinit var selectedDate: Date private var datePicker: MaterialDatePicker? = null private var timePicker: MaterialTimePicker? = null @@ -77,7 +77,7 @@ open class SelectDateAndTimeForScheduledDraftDialog @Inject constructor( onAbort = null } - fun show(title: String, onSchedule: () -> Unit, onAbort: (() -> Unit)? = null) { + fun show(title: String, onSchedule: (Long) -> Unit, onAbort: (() -> Unit)? = null) { showDialogWithBasicInfo(title, R.string.buttonScheduleTitle) setupListeners(onSchedule, onAbort) } @@ -92,7 +92,7 @@ open class SelectDateAndTimeForScheduledDraftDialog @Inject constructor( ) } - private fun setupListeners(onSchedule: () -> Unit, onAbort: (() -> Unit)?) = with(alertDialog) { + private fun setupListeners(onSchedule: (Long) -> Unit, onAbort: (() -> Unit)?) = with(alertDialog) { binding.dateField.setOnClickListener { datePicker?.show(super.activity.supportFragmentManager, "tag") } @@ -125,7 +125,7 @@ open class SelectDateAndTimeForScheduledDraftDialog @Inject constructor( this@SelectDateAndTimeForScheduledDraftDialog.onAbort = onAbort positiveButton.setOnClickListener { - this@SelectDateAndTimeForScheduledDraftDialog.onSchedule?.invoke() + this@SelectDateAndTimeForScheduledDraftDialog.onSchedule?.invoke(selectedDate.time) dismiss() } diff --git a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt index 5ae303d0f4..e2d754d653 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/bottomSheetDialogs/ScheduleSendBottomSheetDialog.kt @@ -86,19 +86,9 @@ class ScheduleSendBottomSheetDialog @Inject constructor() : BottomSheetDialogFra private fun setLastScheduleClickListener() { binding.lastScheduleItem.setOnClickListener { - val draftResource = navigationArgs.draftResource - val matomoName = "lastSelectedSchedule" - - if (navigationArgs.isAlreadyScheduled) { - if (draftResource != null && lastSelectedScheduleEpoch != 0L) { - trackScheduleSendEvent(matomoName) - setBackNavigationResult(SCHEDULE_DRAFT_RESULT, lastSelectedScheduleEpoch) - } - } else { - if (lastSelectedScheduleEpoch != 0L) { - trackScheduleSendEvent(matomoName) - setBackNavigationResult(SCHEDULE_DRAFT_RESULT, lastSelectedScheduleEpoch) - } + if (lastSelectedScheduleEpoch != 0L) { + trackScheduleSendEvent("lastSelectedSchedule") + setBackNavigationResult(SCHEDULE_DRAFT_RESULT, lastSelectedScheduleEpoch) } } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt index 155fe3b9a4..d981feee76 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt @@ -540,11 +540,11 @@ class ThreadFragment : Fragment() { getBackNavigationResult(OPEN_DATE_AND_TIME_SCHEDULE_DIALOG) { _: Boolean -> dateAndTimeScheduleDialog.show( title = getString(R.string.datePickerTitle), - onSchedule = { - val scheduleDate = dateAndTimeScheduleDialog.selectedDate.time - localSettings.lastSelectedScheduleEpoch = scheduleDate + onSchedule = { timestamp -> + localSettings.lastSelectedScheduleEpoch = timestamp + mainViewModel.rescheduleDraft(Date(timestamp)) }, - onAbort = { navigateToScheduleSendBottomSheet(isAlreadyScheduled = false) }, + onAbort = ::navigateToScheduleSendBottomSheet, ) } @@ -726,15 +726,13 @@ class ThreadFragment : Fragment() { private fun rescheduleDraft(draftResource: String) { mainViewModel.draftResource = draftResource - navigateToScheduleSendBottomSheet(isAlreadyScheduled = true) + navigateToScheduleSendBottomSheet() } - private fun navigateToScheduleSendBottomSheet(isAlreadyScheduled: Boolean) { + private fun navigateToScheduleSendBottomSheet() { safeNavigate( resId = R.id.scheduleSendBottomSheetDialog, args = ScheduleSendBottomSheetDialogArgs( - isAlreadyScheduled = isAlreadyScheduled, - draftResource = mainViewModel.draftResource, lastSelectedScheduleEpoch = localSettings.lastSelectedScheduleEpoch ?: 0L, isCurrentMailboxFree = mainViewModel.currentMailbox.value?.isFreeMailbox ?: true, ).toBundle(), diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 6e70caa9b3..09aa4d11a3 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -217,17 +217,6 @@ class NewMessageFragment : Fragment() { private fun setupBackActionHandler() { - fun navigateBackToBottomSheet() { - safeNavigate( - resId = R.id.scheduleSendBottomSheetDialog, - args = ScheduleSendBottomSheetDialogArgs( - isAlreadyScheduled = false, - lastSelectedScheduleEpoch = localSettings.lastSelectedScheduleEpoch ?: 0L, - isCurrentMailboxFree = newMessageViewModel.currentMailbox.isFreeMailbox, - ).toBundle(), - ) - } - fun scheduleDraft(timestamp: Long) { newMessageViewModel.setScheduleDate(Date(timestamp)) tryToSendEmail(scheduled = true) @@ -236,12 +225,11 @@ class NewMessageFragment : Fragment() { getBackNavigationResult(OPEN_DATE_AND_TIME_SCHEDULE_DIALOG) { _: Boolean -> dateAndTimeScheduleDialog.show( title = getString(R.string.datePickerTitle), - onSchedule = { - val scheduleDate = dateAndTimeScheduleDialog.selectedDate.time - localSettings.lastSelectedScheduleEpoch = scheduleDate - scheduleDraft(scheduleDate) + onSchedule = { timestamp -> + localSettings.lastSelectedScheduleEpoch = timestamp + scheduleDraft(timestamp) }, - onAbort = ::navigateBackToBottomSheet, + onAbort = ::navigateToScheduleSendBottomSheet, ) } @@ -731,20 +719,21 @@ class NewMessageFragment : Fragment() { sendButton.isEnabled = it } - scheduleButton.setOnClickListener { - safeNavigate( - resId = R.id.scheduleSendBottomSheetDialog, - args = ScheduleSendBottomSheetDialogArgs( - isAlreadyScheduled = false, - lastSelectedScheduleEpoch = localSettings.lastSelectedScheduleEpoch ?: 0L, - isCurrentMailboxFree = newMessageViewModel.currentMailbox.isFreeMailbox, - ).toBundle(), - ) - } + scheduleButton.setOnClickListener { navigateToScheduleSendBottomSheet() } sendButton.setOnClickListener { tryToSendEmail() } } + private fun navigateToScheduleSendBottomSheet() { + safeNavigate( + resId = R.id.scheduleSendBottomSheetDialog, + args = ScheduleSendBottomSheetDialogArgs( + lastSelectedScheduleEpoch = localSettings.lastSelectedScheduleEpoch ?: 0L, + isCurrentMailboxFree = newMessageViewModel.currentMailbox.isFreeMailbox, + ).toBundle(), + ) + } + private fun tryToSendEmail(scheduled: Boolean = false) { fun setSnackbarActivityResult() { diff --git a/app/src/main/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml index 02f5bb07a5..d6ede54a07 100644 --- a/app/src/main/res/navigation/main_navigation.xml +++ b/app/src/main/res/navigation/main_navigation.xml @@ -251,15 +251,6 @@ android:name="com.infomaniak.mail.ui.bottomSheetDialogs.ScheduleSendBottomSheetDialog" android:label="ScheduleSendBottomSheetDialog" tools:layout="@layout/bottom_sheet_schedule_send"> - - - - + android:defaultValue="0L" + app:argType="long" /> Date: Fri, 7 Feb 2025 14:08:16 +0100 Subject: [PATCH 67/70] fix: Remove overlapping of action bar icons --- .../ui/newMessage/NewMessageEditorManager.kt | 2 +- .../main/res/layout/fragment_new_message.xml | 105 +++++++++--------- 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageEditorManager.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageEditorManager.kt index 1163158bfe..cc084815d1 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageEditorManager.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageEditorManager.kt @@ -127,7 +127,7 @@ class NewMessageEditorManager @Inject constructor(private val insertLinkDialog: editorActions.isGone = isExpanded sendLayout.isGone = isExpanded - formatOptionsScrollView.isVisible = isExpanded + formatOptionsLayout.isVisible = isExpanded } fun observeEditorStatus(): Unit = with(binding) { diff --git a/app/src/main/res/layout/fragment_new_message.xml b/app/src/main/res/layout/fragment_new_message.xml index 0d0aaa59dc..256d48d0ff 100644 --- a/app/src/main/res/layout/fragment_new_message.xml +++ b/app/src/main/res/layout/fragment_new_message.xml @@ -542,76 +542,75 @@ app:icon="@drawable/ic_editor_text_options" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/dynamicEditorActionsLayout" - app:layout_constraintHorizontal_bias="0" - app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - + - + android:orientation="horizontal" + tools:visibility="gone"> - + - + - - + - + + + android:orientation="horizontal" + android:visibility="gone" + tools:visibility="visible"> - - + + From abb76037e5759b10b75175e117ed0a9cf52ae821 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 7 Feb 2025 14:57:23 +0100 Subject: [PATCH 68/70] refactor: Use a LinearLayout instead of a ConstraintLayout in ActionItemView --- .../ui/main/thread/actions/ActionItemView.kt | 4 +- .../res/layout/item_bottom_sheet_action.xml | 105 +++++++++--------- 2 files changed, 55 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ActionItemView.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ActionItemView.kt index 703ce56c89..e240a2555f 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ActionItemView.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/ActionItemView.kt @@ -23,11 +23,11 @@ import android.content.res.TypedArray import android.util.AttributeSet import android.view.LayoutInflater import android.widget.FrameLayout +import android.widget.LinearLayout import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.annotation.StyleableRes import androidx.appcompat.content.res.AppCompatResources -import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isGone import androidx.core.view.isVisible import com.infomaniak.lib.core.utils.getAttributes @@ -84,7 +84,7 @@ class ActionItemView @JvmOverloads constructor( } override fun setOnClickListener(onClickListener: OnClickListener?) { - findViewById(R.id.itemBottomSheetAction).setOnClickListener(onClickListener) + findViewById(R.id.itemBottomSheetAction).setOnClickListener(onClickListener) } fun setIconResource(@DrawableRes iconResourceId: Int) = binding.icon.setImageResource(iconResourceId) diff --git a/app/src/main/res/layout/item_bottom_sheet_action.xml b/app/src/main/res/layout/item_bottom_sheet_action.xml index 964b61f8bf..9391e50f9a 100644 --- a/app/src/main/res/layout/item_bottom_sheet_action.xml +++ b/app/src/main/res/layout/item_bottom_sheet_action.xml @@ -15,7 +15,7 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + android:focusable="true" + android:orientation="vertical"> + app:dividerColor="@color/dividerColor" /> - - - + android:orientation="horizontal" + android:paddingHorizontal="@dimen/marginStandardMedium"> - + + + + android:layout_gravity="center_vertical" + android:layout_marginStart="@dimen/marginStandardMedium"> - - - + + + + + + + From f6429905c6c5b7df8ee542c5681ae699c75cb7fa Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Mon, 10 Feb 2025 09:57:30 +0100 Subject: [PATCH 69/70] fix: Remove useless clickable & focusable --- app/src/main/res/layout/item_bottom_sheet_action.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/res/layout/item_bottom_sheet_action.xml b/app/src/main/res/layout/item_bottom_sheet_action.xml index 9391e50f9a..51277d61b4 100644 --- a/app/src/main/res/layout/item_bottom_sheet_action.xml +++ b/app/src/main/res/layout/item_bottom_sheet_action.xml @@ -22,8 +22,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?attr/selectableItemBackground" - android:clickable="true" - android:focusable="true" android:orientation="vertical"> Date: Mon, 10 Feb 2025 12:48:57 +0100 Subject: [PATCH 70/70] fix: Update strings --- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index dc83a65a04..eb7fb9489a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -159,7 +159,7 @@ Neu planen E-Mails wiederherstellen Planen Sie den Versand von E-Mails für einen späteren Zeitpunkt - Einplanen + Programmieren Siehe Alle Teilen Sie diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index fd464e5592..199bf034db 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -403,7 +403,6 @@ Sin selección ¿Estás seguro de que quieres separar la dirección %s? Separar dirección - Este espacio pertenece a %s y centraliza los archivos comunes de tu organización. Lea las preguntas frecuentes Seguir leyendo Búsquedas recientes