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 ec13a377fc..c0254becd7 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 = 8L - const val MAILBOX_CONTENT_SCHEMA_VERSION = 22L + const val MAILBOX_CONTENT_SCHEMA_VERSION = 23L //endregion //region Configurations names 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 4a1a4bab3b..e01e946a36 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 @@ -1,6 +1,6 @@ /* * Infomaniak Mail - Android - * Copyright (C) 2023-2024 Infomaniak Network SA + * Copyright (C) 2023-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 @@ -23,6 +23,7 @@ import io.realm.kotlin.dynamic.DynamicRealmObject import io.realm.kotlin.dynamic.getValue import io.realm.kotlin.migration.AutomaticSchemaMigration import io.realm.kotlin.migration.AutomaticSchemaMigration.MigrationContext +import io.realm.kotlin.types.RealmInstant val USER_INFO_MIGRATION = AutomaticSchemaMigration { migrationContext -> SentryDebug.addMigrationBreadcrumb(migrationContext) @@ -39,6 +40,7 @@ val MAILBOX_CONTENT_MIGRATION = AutomaticSchemaMigration { migrationContext -> SentryDebug.addMigrationBreadcrumb(migrationContext) migrationContext.deleteRealmFromFirstMigration() migrationContext.keepDefaultValuesAfterNineteenthMigration() + migrationContext.initializedInternalDateAsDateAfterTwentyThirdMigration() } // Migrate to version #1 @@ -98,3 +100,30 @@ private fun MigrationContext.keepDefaultValuesAfterNineteenthMigration() { } } //endregion + +// Migrate from version #23 +private fun MigrationContext.initializedInternalDateAsDateAfterTwentyThirdMigration() { + + if (oldRealm.schemaVersion() <= 23L) { + enumerate(className = "Message") { oldObject: DynamicRealmObject, newObject: DynamicMutableRealmObject? -> + newObject?.apply { + // Initialize new property with old property value + set(propertyName = "internalDate", value = oldObject.getValue(fieldName = "date")) + + // Initialize new property with old property value + set(propertyName = "originalDate", value = oldObject.getValue(fieldName = "date")) + } + } + + enumerate(className = "Thread") { oldObject: DynamicRealmObject, newObject: DynamicMutableRealmObject? -> + newObject?.apply { + // Initialize new property with old property value + set(propertyName = "internalDate", value = oldObject.getValue(fieldName = "date")) + + // Initialize new property with old property value + set(propertyName = "originalDate", value = oldObject.getValue(fieldName = "date")) + } + } + } +} +//endregion diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/CustomRefreshStrategies.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/CustomRefreshStrategies.kt index 18a711902b..691fa3532e 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/CustomRefreshStrategies.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/CustomRefreshStrategies.kt @@ -93,7 +93,6 @@ val snoozeRefreshStrategy = object : DefaultRefreshStrategy { MessageController.getMessage(remoteMessage.uid, realm)?.let { localMessage -> remoteMessage.initLocalValues( messageInitialState = MessageInitialState( - date = localMessage.date, isFullyDownloaded = localMessage.isFullyDownloaded(), isTrashed = localMessage.isTrashed, isFromSearch = localMessage.isFromSearch, 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 a3f37c99ad..44c640c65a 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 @@ -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 @@ -48,7 +48,7 @@ class MessageController @Inject constructor(private val mailboxContentRealm: Rea private fun getSortedAndNotDeletedMessagesQuery(threadUid: String): RealmQuery? { return ThreadController.getThread(threadUid, mailboxContentRealm()) ?.messages?.query("${Message::isDeletedOnApi.name} == false") - ?.sort(Message::date.name, Sort.ASCENDING) + ?.sort(Message::internalDate.name, Sort.ASCENDING) } //endregion 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 7170e2bbcb..a25dd0483c 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 @@ -537,7 +537,6 @@ class RefreshController @Inject constructor( private fun initMessageLocalValues(remoteMessage: Message, folder: Folder) { remoteMessage.initLocalValues( MessageInitialState( - date = remoteMessage.date, isFullyDownloaded = false, isTrashed = folder.role == FolderRole.TRASH, isFromSearch = false, diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt index 396fc610df..62119aab29 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt @@ -62,7 +62,7 @@ class ReplyForwardFooterManager @Inject constructor(private val appContext: Cont } fun createReplyFooter(message: Message): String { - val date = message.date.toDate().formatForHeader() + val date = message.displayDate.toDate().formatForHeader() val from = message.fromName() val messageReplyHeader = appContext.getString(R.string.messageReplyHeader, date, from) @@ -117,7 +117,7 @@ class ReplyForwardFooterManager @Inject constructor(private val appContext: Cont addAndEscapeTextLine("") addAndEscapeTextLine("---------- $messageForwardHeader ---------") addAndEscapeTextLine("$fromTitle ${message.fromName()}") - addAndEscapeTextLine("$dateTitle ${message.date.toDate().formatForHeader()}") + addAndEscapeTextLine("$dateTitle ${message.displayDate.toDate().formatForHeader()}") addAndEscapeTextLine("$subjectTitle ${message.subject}") addAndEscapeRecipientLine(toTitle, message.to) addAndEscapeRecipientLine(ccTitle, message.cc) 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 acfe48ab4f..896ce8b130 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 @@ -108,7 +108,6 @@ class ThreadController @Inject constructor( // which is the reason why we can compute the `isTrashed` value so loosely. remoteMessage.initLocalValues( MessageInitialState( - date = localMessage?.date ?: remoteMessage.date, isFullyDownloaded = localMessage?.isFullyDownloaded() ?: false, isTrashed = filterFolder?.role == FolderRole.TRASH, isFromSearch = localMessage == null, @@ -206,7 +205,7 @@ class ThreadController @Inject constructor( } private fun getSearchThreadsQuery(realm: TypedRealm): RealmQuery { - return realm.query("${Thread::isFromSearch.name} == true").sort(Thread::date.name, Sort.DESCENDING) + return realm.query("${Thread::isFromSearch.name} == true").sort(Thread::internalDate.name, Sort.DESCENDING) } private fun getUnreadThreadsCountQuery(folder: Folder): RealmScalarQuery { @@ -222,7 +221,7 @@ class ThreadController @Inject constructor( val notFromSearch = "${Thread::isFromSearch.name} == false" val notLocallyMovedOut = " AND ${Thread::isLocallyMovedOut.name} == false" - val realmQuery = folder.threads.query(notFromSearch + notLocallyMovedOut).sort(Thread::date.name, sortOrder) + val realmQuery = folder.threads.query(notFromSearch + notLocallyMovedOut).sort(Thread::internalDate.name, sortOrder) return if (filter == ThreadFilter.ALL) { realmQuery @@ -345,7 +344,6 @@ class ThreadController @Inject constructor( remoteMessage.initLocalValues( MessageInitialState( - date = localMessage.date, isFullyDownloaded = true, isTrashed = localMessage.isTrashed, isFromSearch = localMessage.isFromSearch, 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 d60418817b..9c9645fa1b 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 @@ -57,8 +57,12 @@ class Message : RealmObject { var uid: String = "" @SerialName("msg_id") var messageId: String? = null - // This is hardcoded by default to `now`, because the mail protocol allows a date to be null 🤷 - var date: RealmInstant = Date().toRealmInstant() + @SerialName("date") + var originalDate: RealmInstant? = null + private set + @SerialName("internal_date") + var internalDate: RealmInstant = Date().toRealmInstant() // This date is always defined, so the default value is meaningless + private set var subject: String? = null var from = realmListOf() var to = realmListOf() @@ -171,6 +175,14 @@ class Message : RealmObject { @Ignore var snoozeState: SnoozeState? by apiEnum(::_snoozeState) + + /** + * [displayDate] is different than [internalDate] because it must be used when displaying the date of an email but it can't be used + * to sort messages chronologically. + * A message's [originalDate] is not always defined. When this happens, we want to display the [internalDate] in its place. + */ + val displayDate: RealmInstant get() = originalDate ?: internalDate + val threads by backlinks(Thread::messages) private val threadsDuplicatedIn by backlinks(Thread::duplicates) @@ -237,7 +249,6 @@ class Message : RealmObject { swissTransferFiles: RealmList = realmListOf(), ) { - this.date = messageInitialState.date this._isFullyDownloaded = messageInitialState.isFullyDownloaded this.isTrashed = messageInitialState.isTrashed messageInitialState.draftLocalUuid?.let { this.draftLocalUuid = it } @@ -358,7 +369,6 @@ class Message : RealmObject { override fun hashCode(): Int = uid.hashCode() data class MessageInitialState( - val date: RealmInstant, val isFullyDownloaded: Boolean, val isTrashed: Boolean, val isFromSearch: Boolean, 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 9ab1341152..c7056a0921 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 @@ -57,8 +57,9 @@ class Thread : RealmObject { @PrimaryKey var uid: String = "" var messages = realmListOf() - // This is hardcoded by default to `now`, because the mail protocol allows a date to be null 🤷 - var date: RealmInstant = Date().toRealmInstant() + private var originalDate: RealmInstant? = null + // This value should always be provided because messages always have a least an internalDate. Because of this, the initial value is meaningless + var internalDate: RealmInstant = Date().toRealmInstant() @SerialName("unseen_messages") var unseenMessagesCount: Int = 0 var from = realmListOf() @@ -105,6 +106,8 @@ class Thread : RealmObject { var snoozeState: SnoozeState? by apiEnum(::_snoozeState) private set + val displayDate: RealmInstant get() = originalDate ?: internalDate + private val _folders by backlinks(Folder::threads) val folder get() = runCatching { @@ -214,7 +217,7 @@ class Thread : RealmObject { } } - messages.sortBy { it.date } + messages.sortBy { it.internalDate } messages.forEach { message -> messagesIds += message.messageIds @@ -239,7 +242,9 @@ class Thread : RealmObject { duplicates.forEach(::updateSnoozeStatesBasedOn) - date = messages.last { it.folderId == folderId }.date + val lastMessage = messages.last { it.folderId == folderId } + originalDate = lastMessage.originalDate + internalDate = lastMessage.internalDate subject = messages.first().subject } 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 d19b338df4..cff39a1903 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 @@ -237,7 +237,7 @@ class ThreadListAdapter @Inject constructor( mailBodyPreview.text = computePreview().ifBlank { context.getString(R.string.noBodyTitle) } val dateDisplay = computeDateDisplay() - mailDate.text = dateDisplay.formatDate(context, date) + mailDate.text = dateDisplay.formatDate(context, displayDate) mailDateIcon.apply { isVisible = dateDisplay.icon != null dateDisplay.icon?.let { setImageResource(it) } @@ -715,7 +715,7 @@ class ThreadListAdapter @Inject constructor( } } - private fun Thread.getSectionTitle(context: Context): String = with(date.toDate()) { + private fun Thread.getSectionTitle(context: Context): String = with(internalDate.toDate()) { return when { isInTheFuture() -> context.getString(R.string.comingSoon) isToday() -> context.getString(R.string.threadListSectionToday) 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 595fccc2d0..957d372dbe 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 @@ -350,13 +350,13 @@ class ThreadAdapter( } private fun MessageViewHolder.bindHeader(message: Message) = with(binding) { - val messageDate = message.date.toDate() + val messageDate = message.displayDate.toDate() if (message.isScheduledDraft) { scheduleAlert.setDescription( context.getString( R.string.scheduledEmailHeader, - message.date.toDate().format(FORMAT_DATE_DAY_FULL_MONTH_YEAR_WITH_TIME), + message.displayDate.toDate().format(FORMAT_DATE_DAY_FULL_MONTH_YEAR_WITH_TIME), ), ) scheduleSendIcon.isVisible = true diff --git a/app/src/main/java/com/infomaniak/mail/utils/PrintHeaderUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/PrintHeaderUtils.kt index 7c520919c0..64156f5e02 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/PrintHeaderUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/PrintHeaderUtils.kt @@ -52,7 +52,7 @@ object PrintHeaderUtils { messageDetailsDiv.insertPrintRecipientField(context.getString(R.string.toTitle), *message.to.toTypedArray()) message.sender?.let { messageDetailsDiv.insertPrintRecipientField(context.getString(R.string.fromTitle), it) } - messageDetailsDiv.insertPrintDateField(context.getString(R.string.dateTitle), message.date.toDate()) + messageDetailsDiv.insertPrintDateField(context.getString(R.string.dateTitle), message.displayDate.toDate()) elementsToInsert.add(messageDetailsDiv)