Skip to content

Commit

Permalink
refactor: Extract handleAddedMessage into its refresh strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinBoulongne committed Mar 6, 2025
1 parent 76fe917 commit 8eb030d
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.infomaniak.mail.data.models.message.Message
import com.infomaniak.mail.data.models.thread.Thread
import io.realm.kotlin.MutableRealm
import io.realm.kotlin.TypedRealm
import kotlinx.coroutines.CoroutineScope

val defaultRefreshStrategy = object : DefaultRefreshStrategy {}

Expand All @@ -35,11 +36,17 @@ val snoozeRefreshStrategy = object : DefaultRefreshStrategy {
return ThreadController.getInboxThreadsWithSnoozeFilter(withSnooze = true, realm = realm)
}

override fun updateExistingMessageWhenAdded(remoteMessage: Message, realm: MutableRealm) {
MessageController.updateMessage(remoteMessage.uid, realm = realm) { localMessage ->
override fun handleAddedMessages(
scope: CoroutineScope,
remoteMessage: Message,
isConversationMode: Boolean,
realm: MutableRealm,
): Set<Thread> = buildSet {
MessageController.updateMessage(remoteMessage.uid, realm) { localMessage ->
localMessage?.snoozeState = remoteMessage.snoozeState
localMessage?.snoozeEndDate = remoteMessage.snoozeEndDate
localMessage?.snoozeAction = remoteMessage.snoozeAction
localMessage?.threads?.let(::addAll)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ import io.realm.kotlin.Realm
import io.realm.kotlin.TypedRealm
import io.realm.kotlin.ext.copyFromRealm
import io.realm.kotlin.ext.toRealmList
import io.realm.kotlin.query.RealmResults
import io.realm.kotlin.types.RealmList
import io.realm.kotlin.types.RealmSet
import io.sentry.Sentry
import kotlinx.coroutines.*
import okhttp3.OkHttpClient
Expand Down Expand Up @@ -518,20 +516,8 @@ class RefreshController @Inject constructor(
scope.ensureActive()

initMessageLocalValues(remoteMessage, folder)

addedMessagesUids.add(remoteMessage.shortUid)

refreshStrategy.updateExistingMessageWhenAdded(remoteMessage, realm = this)

val newThread = if (isConversationMode) {
val (thread, impactThreads) = handleAddedMessage(scope, remoteMessage)
impactedThreadsManaged += impactThreads
thread
} else {
remoteMessage.toThread()
}

newThread?.let { impactedThreadsManaged += putNewThreadInRealm(it) }
impactedThreadsManaged += refreshStrategy.handleAddedMessages(scope, remoteMessage, isConversationMode, realm = this)
}

addSentryBreadcrumbForAddedUidsInFolder(addedMessagesUids)
Expand Down Expand Up @@ -559,129 +545,6 @@ class RefreshController @Inject constructor(
latestCalendarEventResponse = null,
)
}

private fun MutableRealm.handleAddedMessage(scope: CoroutineScope, remoteMessage: Message): Pair<Thread?, Set<Thread>> {

// Other pre-existing Threads that will also require this Message and will provide the prior Messages for this new Thread.
val existingThreads = ThreadController.getThreadsByMessageIds(remoteMessage.messageIds, realm = this)
val existingMessages = getExistingMessages(existingThreads)

val thread = createNewThreadIfRequired(scope, remoteMessage, existingThreads, existingMessages)
val impactedThreads = updateExistingThreads(scope, remoteMessage, existingThreads, existingMessages)

return thread to impactedThreads
}

private fun TypedRealm.createNewThreadIfRequired(
scope: CoroutineScope,
newMessage: Message,
existingThreads: List<Thread>,
existingMessages: Set<Message>,
): Thread? {
var newThread: Thread? = null

if (existingThreads.none { it.folderId == newMessage.folderId }) {

newThread = newMessage.toThread()

addPreviousMessagesToThread(scope, newThread, existingMessages)
}

return newThread
}

private fun MutableRealm.updateExistingThreads(
scope: CoroutineScope,
remoteMessage: Message,
existingThreads: RealmResults<Thread>,
existingMessages: Set<Message>,
): Set<Thread> {

val impactedThreads = mutableSetOf<Thread>()

// Update already existing Threads (i.e. in other Folders, or specific cases like Snoozed)
impactedThreads += addAllMessagesToAllThreads(scope, remoteMessage, existingThreads, existingMessages)

// Some Messages don't have references to all previous Messages of the Thread (ex: these from the iOS Mail app).
// Because we are missing the links between Messages, it will create multiple Threads for the same Folder.
// Hence, we need to find these duplicates, and remove them.
val duplicatedThreads = identifyExtraDuplicatedThreads(remoteMessage.messageIds)
impactedThreads -= duplicatedThreads
duplicatedThreads.forEach(::delete) // Delete the other Threads. Sorry bro, you won't be missed.

return impactedThreads
}

private fun MutableRealm.addAllMessagesToAllThreads(
scope: CoroutineScope,
remoteMessage: Message,
existingThreads: RealmResults<Thread>,
existingMessages: Set<Message>,
): Set<Thread> {

if (existingThreads.isEmpty()) return emptySet()

val allExistingMessages = buildSet {
addAll(existingMessages)
add(remoteMessage)
}

return buildSet {
existingThreads.forEach { thread ->
scope.ensureActive()

allExistingMessages.forEach { existingMessage ->
scope.ensureActive()

if (!thread.messages.contains(existingMessage)) {
thread.messagesIds += existingMessage.messageIds
thread.addMessageWithConditions(existingMessage, realm = this@addAllMessagesToAllThreads)
}
}

add(thread)
}
}
}

private fun MutableRealm.identifyExtraDuplicatedThreads(messageIds: RealmSet<String>): Set<Thread> {

// Create a map with all duplicated Threads of the same Thread in a list.
val map = mutableMapOf<String, MutableList<Thread>>()
ThreadController.getThreadsByMessageIds(messageIds, realm = this).forEach {
map.getOrPut(it.folderId) { mutableListOf() }.add(it)
}

return buildSet {
map.values.forEach { threads ->
// We want to keep only 1 duplicated Thread, so we skip the 1st one. (He's the chosen one!)
addAll(threads.subList(1, threads.count()))
}
}
}

private fun MutableRealm.putNewThreadInRealm(newThread: Thread): Thread {
return ThreadController.upsertThread(newThread, realm = this)
}

private fun getExistingMessages(existingThreads: List<Thread>): Set<Message> {
return existingThreads.flatMapTo(mutableSetOf()) { it.messages }
}

private fun TypedRealm.addPreviousMessagesToThread(
scope: CoroutineScope,
newThread: Thread,
referenceMessages: Set<Message>,
) {
referenceMessages.forEach { message ->
scope.ensureActive()

newThread.apply {
messagesIds += message.computeMessageIds()
addMessageWithConditions(message, realm = this@addPreviousMessagesToThread)
}
}
}
//endregion

//region API calls
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,168 @@ import com.infomaniak.mail.data.models.message.Message
import com.infomaniak.mail.data.models.thread.Thread
import io.realm.kotlin.MutableRealm
import io.realm.kotlin.TypedRealm
import io.realm.kotlin.query.RealmResults
import io.realm.kotlin.types.RealmSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive

interface RefreshStrategy {
fun queryFolderThreads(folderId: String, realm: TypedRealm): List<Thread>
fun updateExistingMessageWhenAdded(remoteMessage: Message, realm: MutableRealm)

/**
* The returned Threads should be managed by Realm
*/
fun handleAddedMessages(
scope: CoroutineScope,
remoteMessage: Message,
isConversationMode: Boolean,
realm: MutableRealm,
): Set<Thread>
}

interface DefaultRefreshStrategy : RefreshStrategy {
override fun queryFolderThreads(folderId: String, realm: TypedRealm): List<Thread> {
return ThreadController.getThreadsByFolderId(folderId, realm)
}

override fun updateExistingMessageWhenAdded(remoteMessage: Message, realm: MutableRealm) = Unit
override fun handleAddedMessages(
scope: CoroutineScope,
remoteMessage: Message,
isConversationMode: Boolean,
realm: MutableRealm,
): Set<Thread> {
val impactedThreads = mutableSetOf<Thread>()
val newThread = if (isConversationMode) {
val (thread, otherThreads) = realm.handleAddedMessage(scope, remoteMessage)
impactedThreads += otherThreads
thread
} else {
remoteMessage.toThread()
}
newThread?.let { impactedThreads += realm.putNewThreadInRealm(it) }
return impactedThreads
}

private fun MutableRealm.handleAddedMessage(scope: CoroutineScope, remoteMessage: Message): Pair<Thread?, Set<Thread>> {

// Other pre-existing Threads that will also require this Message and will provide the prior Messages for this new Thread.
val existingThreads = ThreadController.getThreadsByMessageIds(remoteMessage.messageIds, realm = this)
val existingMessages = getExistingMessages(existingThreads)

val thread = createNewThreadIfRequired(scope, remoteMessage, existingThreads, existingMessages)
val impactedThreads = updateExistingThreads(scope, remoteMessage, existingThreads, existingMessages)

return thread to impactedThreads
}

private fun TypedRealm.createNewThreadIfRequired(
scope: CoroutineScope,
newMessage: Message,
existingThreads: List<Thread>,
existingMessages: Set<Message>,
): Thread? {
var newThread: Thread? = null

if (existingThreads.none { it.folderId == newMessage.folderId }) {

newThread = newMessage.toThread()

addPreviousMessagesToThread(scope, newThread, existingMessages)
}

return newThread
}

private fun MutableRealm.updateExistingThreads(
scope: CoroutineScope,
remoteMessage: Message,
existingThreads: RealmResults<Thread>,
existingMessages: Set<Message>,
): Set<Thread> {

val impactedThreads = mutableSetOf<Thread>()

// Update already existing Threads (i.e. in other Folders, or specific cases like Snoozed)
impactedThreads += addAllMessagesToAllThreads(scope, remoteMessage, existingThreads, existingMessages)

// Some Messages don't have references to all previous Messages of the Thread (ex: these from the iOS Mail app).
// Because we are missing the links between Messages, it will create multiple Threads for the same Folder.
// Hence, we need to find these duplicates, and remove them.
val duplicatedThreads = identifyExtraDuplicatedThreads(remoteMessage.messageIds)
impactedThreads -= duplicatedThreads
duplicatedThreads.forEach(::delete) // Delete the other Threads. Sorry bro, you won't be missed.

return impactedThreads
}

private fun MutableRealm.addAllMessagesToAllThreads(
scope: CoroutineScope,
remoteMessage: Message,
existingThreads: RealmResults<Thread>,
existingMessages: Set<Message>,
): Set<Thread> {

if (existingThreads.isEmpty()) return emptySet()

val allExistingMessages = buildSet {
addAll(existingMessages)
add(remoteMessage)
}

return buildSet {
existingThreads.forEach { thread ->
scope.ensureActive()

allExistingMessages.forEach { existingMessage ->
scope.ensureActive()

if (!thread.messages.contains(existingMessage)) {
thread.messagesIds += existingMessage.messageIds
thread.addMessageWithConditions(existingMessage, realm = this@addAllMessagesToAllThreads)
}
}

add(thread)
}
}
}

private fun MutableRealm.identifyExtraDuplicatedThreads(messageIds: RealmSet<String>): Set<Thread> {

// Create a map with all duplicated Threads of the same Thread in a list.
val map = mutableMapOf<String, MutableList<Thread>>()
ThreadController.getThreadsByMessageIds(messageIds, realm = this).forEach {
map.getOrPut(it.folderId) { mutableListOf() }.add(it)
}

return buildSet {
map.values.forEach { threads ->
// We want to keep only 1 duplicated Thread, so we skip the 1st one. (He's the chosen one!)
addAll(threads.subList(1, threads.count()))
}
}
}

private fun MutableRealm.putNewThreadInRealm(newThread: Thread): Thread {
return ThreadController.upsertThread(newThread, realm = this)
}

private fun getExistingMessages(existingThreads: List<Thread>): Set<Message> {
return existingThreads.flatMapTo(mutableSetOf()) { it.messages }
}

private fun TypedRealm.addPreviousMessagesToThread(
scope: CoroutineScope,
newThread: Thread,
referenceMessages: Set<Message>,
) {
referenceMessages.forEach { message ->
scope.ensureActive()

newThread.apply {
messagesIds += message.computeMessageIds()
addMessageWithConditions(message, realm = this@addPreviousMessagesToThread)
}
}
}
}

0 comments on commit 8eb030d

Please sign in to comment.