Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Extract handleAddedMessage into its refresh strategy #2229

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,20 @@ 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 ->
localMessage?.snoozeState = remoteMessage.snoozeState
localMessage?.snoozeEndDate = remoteMessage.snoozeEndDate
localMessage?.snoozeAction = remoteMessage.snoozeAction
override fun handleAddedMessages(
scope: CoroutineScope,
remoteMessage: Message,
isConversationMode: Boolean,
impactedThreadsManaged: MutableSet<Thread>,
realm: MutableRealm,
) {
impactedThreadsManaged += 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,18 +516,8 @@ class RefreshController @Inject constructor(
scope.ensureActive()

initMessageLocalValues(remoteMessage, folder)

addedMessagesUids.add(remoteMessage.shortUid)

refreshStrategy.updateExistingMessageWhenAdded(remoteMessage, realm = this)

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

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

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

private fun MutableRealm.handleAddedMessage(
scope: CoroutineScope,
remoteMessage: Message,
impactedThreadsManaged: MutableSet<Thread>,
): 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)

// 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.
val isThereDuplicatedThreads = isThereDuplicatedThreads(remoteMessage.messageIds, existingThreads.count())

// Create Thread in this Folder
val thread = createNewThreadIfRequired(scope, remoteMessage, existingThreads, existingMessages)
// Update Threads in other Folders
addAllMessagesToAllThreads(scope, remoteMessage, existingThreads, existingMessages, impactedThreadsManaged)

// Now that all other existing Threads are updated, we need to remove the duplicated Threads.
if (isThereDuplicatedThreads) removeDuplicatedThreads(remoteMessage.messageIds, impactedThreadsManaged)

return thread
}

private fun MutableRealm.isThereDuplicatedThreads(messageIds: RealmSet<String>, threadsCount: Int): Boolean {
val foldersCount = ThreadController.getExistingThreadsFoldersCount(messageIds, realm = this)
return foldersCount != threadsCount.toLong()
}

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.addAllMessagesToAllThreads(
scope: CoroutineScope,
remoteMessage: Message,
existingThreads: RealmResults<Thread>,
existingMessages: Set<Message>,
impactedThreadsManaged: MutableSet<Thread>,
) {
if (existingThreads.isEmpty()) return

val allExistingMessages = mutableSetOf<Message>().apply {
addAll(existingMessages)
add(remoteMessage)
}

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

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

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

impactedThreadsManaged += thread
}
}

private fun MutableRealm.removeDuplicatedThreads(messageIds: RealmSet<String>, impactedThreadsManaged: MutableSet<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)
}

map.values.forEach { threads ->
threads.forEachIndexed { index, thread ->
if (index > 0) { // We want to keep only 1 duplicated Thread, so we skip the 1st one. (He's the chosen one!)
impactedThreadsManaged.remove(thread)
delete(thread) // Delete the other Threads. Sorry bro, you won't be missed.
}
}
}
}

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,165 @@ 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)

/**
* About the [impactedThreadsManaged]:
* This set will be updated throughout the whole process of handling added Messages.
* It represents all the Threads that will need to be recomputed to reflect the changes of the newly added Messages.
* We need to pass down a reference to the MutableSet to enable both addition and removal of Threads in it.
*/
fun handleAddedMessages(
scope: CoroutineScope,
remoteMessage: Message,
isConversationMode: Boolean,
impactedThreadsManaged: MutableSet<Thread>,
realm: MutableRealm,
)
}

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,
impactedThreadsManaged: MutableSet<Thread>,
realm: MutableRealm,
) {
val newThread = if (isConversationMode) {
realm.handleAddedMessage(scope, remoteMessage, impactedThreadsManaged)
} else {
remoteMessage.toThread()
}
newThread?.let { impactedThreadsManaged += realm.putNewThreadInRealm(it) }
}

private fun MutableRealm.handleAddedMessage(
scope: CoroutineScope,
remoteMessage: Message,
impactedThreadsManaged: MutableSet<Thread>,
): 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)

// 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.
val isThereDuplicatedThreads = isThereDuplicatedThreads(remoteMessage.messageIds, existingThreads.count())

// Create Thread in this Folder
val thread = createNewThreadIfRequired(scope, remoteMessage, existingThreads, existingMessages)
// Update Threads in other Folders
addAllMessagesToAllThreads(scope, remoteMessage, existingThreads, existingMessages, impactedThreadsManaged)

// Now that all other existing Threads are updated, we need to remove the duplicated Threads.
if (isThereDuplicatedThreads) removeDuplicatedThreads(remoteMessage.messageIds, impactedThreadsManaged)

return thread
}

private fun MutableRealm.isThereDuplicatedThreads(messageIds: RealmSet<String>, threadsCount: Int): Boolean {
val foldersCount = ThreadController.getExistingThreadsFoldersCount(messageIds, realm = this)
return foldersCount != threadsCount.toLong()
}

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.addAllMessagesToAllThreads(
scope: CoroutineScope,
remoteMessage: Message,
existingThreads: RealmResults<Thread>,
existingMessages: Set<Message>,
impactedThreadsManaged: MutableSet<Thread>,
) {
if (existingThreads.isEmpty()) return

val allExistingMessages = mutableSetOf<Message>().apply {
addAll(existingMessages)
add(remoteMessage)
}

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

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

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

impactedThreadsManaged += thread
}
}

private fun MutableRealm.removeDuplicatedThreads(messageIds: RealmSet<String>, impactedThreadsManaged: MutableSet<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)
}

map.values.forEach { threads ->
threads.forEachIndexed { index, thread ->
if (index > 0) { // We want to keep only 1 duplicated Thread, so we skip the 1st one. (He's the chosen one!)
impactedThreadsManaged.remove(thread)
delete(thread) // Delete the other Threads. Sorry bro, you won't be missed.
}
}
}
}

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)
}
}
}
}