Skip to content

Commit

Permalink
feat: Order threads by their new internal date field (#2239)
Browse files Browse the repository at this point in the history
  • Loading branch information
LunarX authored Mar 11, 2025
2 parents 34c5fe5 + 29dfb29 commit 1e23655
Show file tree
Hide file tree
Showing 12 changed files with 65 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -39,6 +40,7 @@ val MAILBOX_CONTENT_MIGRATION = AutomaticSchemaMigration { migrationContext ->
SentryDebug.addMigrationBreadcrumb(migrationContext)
migrationContext.deleteRealmFromFirstMigration()
migrationContext.keepDefaultValuesAfterNineteenthMigration()
migrationContext.initializedInternalDateAsDateAfterTwentyThirdMigration()
}

// Migrate to version #1
Expand Down Expand Up @@ -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<RealmInstant>(fieldName = "date"))

// Initialize new property with old property value
set(propertyName = "originalDate", value = oldObject.getValue<RealmInstant>(fieldName = "date"))
}
}

enumerate(className = "Thread") { oldObject: DynamicRealmObject, newObject: DynamicMutableRealmObject? ->
newObject?.apply {
// Initialize new property with old property value
set(propertyName = "internalDate", value = oldObject.getValue<RealmInstant>(fieldName = "date"))

// Initialize new property with old property value
set(propertyName = "originalDate", value = oldObject.getValue<RealmInstant>(fieldName = "date"))
}
}
}
}
//endregion
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -48,7 +48,7 @@ class MessageController @Inject constructor(private val mailboxContentRealm: Rea
private fun getSortedAndNotDeletedMessagesQuery(threadUid: String): RealmQuery<Message>? {
return ThreadController.getThread(threadUid, mailboxContentRealm())
?.messages?.query("${Message::isDeletedOnApi.name} == false")
?.sort(Message::date.name, Sort.ASCENDING)
?.sort(Message::internalDate.name, Sort.ASCENDING)
}
//endregion

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -206,7 +205,7 @@ class ThreadController @Inject constructor(
}

private fun getSearchThreadsQuery(realm: TypedRealm): RealmQuery<Thread> {
return realm.query<Thread>("${Thread::isFromSearch.name} == true").sort(Thread::date.name, Sort.DESCENDING)
return realm.query<Thread>("${Thread::isFromSearch.name} == true").sort(Thread::internalDate.name, Sort.DESCENDING)
}

private fun getUnreadThreadsCountQuery(folder: Folder): RealmScalarQuery<Long> {
Expand All @@ -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
Expand Down Expand Up @@ -345,7 +344,6 @@ class ThreadController @Inject constructor(

remoteMessage.initLocalValues(
MessageInitialState(
date = localMessage.date,
isFullyDownloaded = true,
isTrashed = localMessage.isTrashed,
isFromSearch = localMessage.isFromSearch,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Recipient>()
var to = realmListOf<Recipient>()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -237,7 +249,6 @@ class Message : RealmObject {
swissTransferFiles: RealmList<SwissTransferFile> = realmListOf(),
) {

this.date = messageInitialState.date
this._isFullyDownloaded = messageInitialState.isFullyDownloaded
this.isTrashed = messageInitialState.isTrashed
messageInitialState.draftLocalUuid?.let { this.draftLocalUuid = it }
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ class Thread : RealmObject {
@PrimaryKey
var uid: String = ""
var messages = realmListOf<Message>()
// 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<Recipient>()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -214,7 +217,7 @@ class Thread : RealmObject {
}
}

messages.sortBy { it.date }
messages.sortBy { it.internalDate }

messages.forEach { message ->
messagesIds += message.messageIds
Expand All @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit 1e23655

Please sign in to comment.