From 3fcfe7cf8f0fd911f8386ab5d49e7fa9bbe1b76a Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Tue, 27 Feb 2024 09:37:54 +0100 Subject: [PATCH 01/10] Save backup when leaving Thread, and restore it when coming back --- .../com/infomaniak/mail/ui/MainViewModel.kt | 20 +++++++++++++++++ .../mail/ui/main/folder/ThreadListFragment.kt | 2 +- .../mail/ui/main/folder/TwoPaneFragment.kt | 11 +++++++--- .../mail/ui/main/thread/ThreadAdapter.kt | 5 +++++ .../mail/ui/main/thread/ThreadFragment.kt | 12 +++++++++- .../mail/ui/main/thread/ThreadViewModel.kt | 22 ++++++++++++------- 6 files changed, 59 insertions(+), 13 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 245710db93..7a74bdfec4 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt @@ -206,6 +206,26 @@ class MainViewModel @Inject constructor( val mergedContactsLive: LiveData = avatarMergedContactData.mergedContactLiveData //endregion + //region Restore Thread state after going to MoveFragment or somewhere else, and then coming back to ThreadFragment. + var threadBackup: ThreadBackup? = null + + data class ThreadBackup( + // TODO + ) + + fun createThreadBackup( + // TODO + ) { + threadBackup = ThreadBackup( + // TODO + ) + } + + fun deleteThreadBackup() { + threadBackup = null + } + //endregion + fun updateUserInfo() = viewModelScope.launch(ioCoroutineContext) { SentryLog.d(TAG, "Update user info") updateAddressBooks() 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 9ed6bb70ce..7598fa2764 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 @@ -191,7 +191,7 @@ class ThreadListFragment : TwoPaneFragment(), SwipeRefreshLayout.OnRefreshListen } // If we are coming from a Notification, we need to navigate to ThreadFragment. - twoPaneViewModel.openThread(threadUid) + openThreadAndDeleteBackup(threadUid) } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index f560fa4076..550756dd33 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -133,15 +133,20 @@ abstract class TwoPaneFragment : Fragment() { } } - fun navigateToThread(thread: Thread) = with(twoPaneViewModel) { + fun navigateToThread(thread: Thread) { if (thread.isOnlyOneDraft) { trackNewMessageEvent(OPEN_FROM_DRAFT_NAME) - openDraft(thread) + twoPaneViewModel.openDraft(thread) } else { - openThread(thread.uid) + openThreadAndDeleteBackup(thread.uid) } } + fun openThreadAndDeleteBackup(threadUid: String) { + mainViewModel.deleteThreadBackup() + twoPaneViewModel.openThread(threadUid) + } + private fun resetPanes() { if (isOnlyLeftShown()) { 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 dc8969172d..33ac5af3be 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 @@ -48,6 +48,7 @@ import com.infomaniak.mail.data.models.correspondent.Recipient import com.infomaniak.mail.data.models.message.Message import com.infomaniak.mail.data.models.message.Message.* import com.infomaniak.mail.databinding.* +import com.infomaniak.mail.ui.MainViewModel.ThreadBackup import com.infomaniak.mail.ui.main.thread.ThreadAdapter.* import com.infomaniak.mail.utils.* import com.infomaniak.mail.utils.MailDateFormatUtils.mailFormattedDate @@ -603,6 +604,10 @@ class ThreadAdapter( indexOfMessage?.let { notifyItemChanged(it, NotifyType.ONLY_REBIND_CALENDAR_ATTENDANCE) } } + fun useThreadBackup(threadBackup: ThreadBackup) = with(threadBackup) { + // TODO + } + private enum class NotifyType { TOGGLE_LIGHT_MODE, RE_RENDER, 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 b9b1eb763d..8376194074 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 @@ -153,6 +153,15 @@ class ThreadFragment : Fragment() { updateNavigationIcon() } + override fun onStop() = with(threadAdapter) { + + mainViewModel.createThreadBackup( + // TODO + ) + + super.onStop() + } + override fun onDestroyView() { threadAdapter.resetCallbacks() super.onDestroyView() @@ -302,6 +311,7 @@ class ThreadFragment : Fragment() { recycledViewPool.setMaxRecycledViews(0, 0) threadAdapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY + mainViewModel.threadBackup?.let(threadAdapter::useThreadBackup) } private fun setupDialogs() { @@ -318,7 +328,7 @@ class ThreadFragment : Fragment() { resetMessagesRelatedCache() displayThreadView() - openThread(threadUid).observe(viewLifecycleOwner) { result -> + openThread(threadUid, mainViewModel.threadBackup).observe(viewLifecycleOwner) { result -> if (result == null) { twoPaneViewModel.closeThread() diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt index 700242c60e..cbe076d7ad 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt @@ -34,6 +34,7 @@ import com.infomaniak.mail.data.models.mailbox.Mailbox import com.infomaniak.mail.data.models.message.Message import com.infomaniak.mail.data.models.thread.Thread import com.infomaniak.mail.di.IoDispatcher +import com.infomaniak.mail.ui.MainViewModel.ThreadBackup import com.infomaniak.mail.ui.main.thread.ThreadAdapter.SuperCollapsedBlock import com.infomaniak.mail.utils.* import com.infomaniak.mail.utils.MessageBodyUtils.SplitBody @@ -243,7 +244,7 @@ class ThreadViewModel @Inject constructor( return@withContext message } - fun openThread(threadUid: String) = liveData(ioCoroutineContext) { + fun openThread(threadUid: String, threadBackup: ThreadBackup?) = liveData(ioCoroutineContext) { val thread = threadController.getThread(threadUid) ?: run { emit(null) @@ -252,14 +253,19 @@ class ThreadViewModel @Inject constructor( sendMatomoAndSentryAboutThreadMessagesCount(thread) - val isExpandedMap = mutableMapOf() - val isThemeTheSameMap = mutableMapOf() - val initialSetOfExpandedMessagesUids = mutableSetOf() - thread.messages.forEachIndexed { index, message -> - isExpandedMap[message.uid] = message.shouldBeExpanded(index, thread.messages.lastIndex).also { - if (it) initialSetOfExpandedMessagesUids.add(message.uid) + var isExpandedMap = mutableMapOf() + var initialSetOfExpandedMessagesUids = mutableSetOf() + var isThemeTheSameMap = mutableMapOf() + + if (threadBackup != null) { + // TODO + } else { + thread.messages.forEachIndexed { index, message -> + isExpandedMap[message.uid] = message.shouldBeExpanded(index, thread.messages.lastIndex).also { + if (it) initialSetOfExpandedMessagesUids.add(message.uid) + } + isThemeTheSameMap[message.uid] = true } - isThemeTheSameMap[message.uid] = true } shouldMarkThreadAsSeen = thread.unseenMessagesCount > 0 From 24005d55385872f53ec7812f80c9c39113edb5e4 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Tue, 27 Feb 2024 09:39:27 +0100 Subject: [PATCH 02/10] Backup Messages' expanded status --- app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt | 6 +++--- .../com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt | 2 +- .../com/infomaniak/mail/ui/main/thread/ThreadFragment.kt | 2 +- .../com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt | 2 +- 4 files changed, 6 insertions(+), 6 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 7a74bdfec4..312f1c6657 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt @@ -210,14 +210,14 @@ class MainViewModel @Inject constructor( var threadBackup: ThreadBackup? = null data class ThreadBackup( - // TODO + val isExpandedMapBackup: MutableMap, ) fun createThreadBackup( - // TODO + isExpandedMap: MutableMap, ) { threadBackup = ThreadBackup( - // TODO + isExpandedMap, ) } 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 33ac5af3be..3e05e3ad25 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 @@ -605,7 +605,7 @@ class ThreadAdapter( } fun useThreadBackup(threadBackup: ThreadBackup) = with(threadBackup) { - // TODO + isExpandedMap = isExpandedMapBackup } private enum class NotifyType { 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 8376194074..8017ac18a6 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 @@ -156,7 +156,7 @@ class ThreadFragment : Fragment() { override fun onStop() = with(threadAdapter) { mainViewModel.createThreadBackup( - // TODO + isExpandedMap = isExpandedMap, ) super.onStop() diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt index cbe076d7ad..1c1695bef1 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt @@ -258,7 +258,7 @@ class ThreadViewModel @Inject constructor( var isThemeTheSameMap = mutableMapOf() if (threadBackup != null) { - // TODO + isExpandedMap = threadBackup.isExpandedMapBackup } else { thread.messages.forEachIndexed { index, message -> isExpandedMap[message.uid] = message.shouldBeExpanded(index, thread.messages.lastIndex).also { From b1a2c4d066cb8348486bbc93e7068d79d2a4be5e Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Tue, 27 Feb 2024 09:44:14 +0100 Subject: [PATCH 03/10] Backup initial set of expanded Messages --- app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt | 3 +++ .../java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt | 1 + .../java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt | 1 + .../java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt | 1 + 4 files changed, 6 insertions(+) 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 312f1c6657..2538652b15 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt @@ -211,13 +211,16 @@ class MainViewModel @Inject constructor( data class ThreadBackup( val isExpandedMapBackup: MutableMap, + val initialSetOfExpandedMessagesUidsBackup: Set, ) fun createThreadBackup( isExpandedMap: MutableMap, + initialSetOfExpandedMessagesUids: Set, ) { threadBackup = ThreadBackup( isExpandedMap, + initialSetOfExpandedMessagesUids, ) } 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 3e05e3ad25..69b9e71d69 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 @@ -606,6 +606,7 @@ class ThreadAdapter( fun useThreadBackup(threadBackup: ThreadBackup) = with(threadBackup) { isExpandedMap = isExpandedMapBackup + initialSetOfExpandedMessagesUids = initialSetOfExpandedMessagesUidsBackup } private enum class NotifyType { 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 8017ac18a6..e6c0f2aa85 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 @@ -157,6 +157,7 @@ class ThreadFragment : Fragment() { mainViewModel.createThreadBackup( isExpandedMap = isExpandedMap, + initialSetOfExpandedMessagesUids = isExpandedMap.filter { it.value }.keys.toMutableSet(), ) super.onStop() diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt index 1c1695bef1..a8c36c3988 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt @@ -259,6 +259,7 @@ class ThreadViewModel @Inject constructor( if (threadBackup != null) { isExpandedMap = threadBackup.isExpandedMapBackup + initialSetOfExpandedMessagesUids = threadBackup.initialSetOfExpandedMessagesUidsBackup.toMutableSet() } else { thread.messages.forEachIndexed { index, message -> isExpandedMap[message.uid] = message.shouldBeExpanded(index, thread.messages.lastIndex).also { From 0c5840f5307e1e7942a90a320f7883e273bd420a Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Tue, 27 Feb 2024 09:53:11 +0100 Subject: [PATCH 04/10] Backup Messages' theme status --- app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt | 3 +++ .../java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt | 1 + .../java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt | 1 + .../java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt | 1 + 4 files changed, 6 insertions(+) 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 2538652b15..f43c37338d 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt @@ -212,15 +212,18 @@ class MainViewModel @Inject constructor( data class ThreadBackup( val isExpandedMapBackup: MutableMap, val initialSetOfExpandedMessagesUidsBackup: Set, + val isThemeTheSameMapBackup: MutableMap, ) fun createThreadBackup( isExpandedMap: MutableMap, initialSetOfExpandedMessagesUids: Set, + isThemeTheSameMap: MutableMap, ) { threadBackup = ThreadBackup( isExpandedMap, initialSetOfExpandedMessagesUids, + isThemeTheSameMap, ) } 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 69b9e71d69..aa90057780 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 @@ -607,6 +607,7 @@ class ThreadAdapter( fun useThreadBackup(threadBackup: ThreadBackup) = with(threadBackup) { isExpandedMap = isExpandedMapBackup initialSetOfExpandedMessagesUids = initialSetOfExpandedMessagesUidsBackup + isThemeTheSameMap = isThemeTheSameMapBackup } private enum class NotifyType { 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 e6c0f2aa85..7b09a57d51 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 @@ -158,6 +158,7 @@ class ThreadFragment : Fragment() { mainViewModel.createThreadBackup( isExpandedMap = isExpandedMap, initialSetOfExpandedMessagesUids = isExpandedMap.filter { it.value }.keys.toMutableSet(), + isThemeTheSameMap = isThemeTheSameMap, ) super.onStop() diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt index a8c36c3988..30eb614872 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt @@ -260,6 +260,7 @@ class ThreadViewModel @Inject constructor( if (threadBackup != null) { isExpandedMap = threadBackup.isExpandedMapBackup initialSetOfExpandedMessagesUids = threadBackup.initialSetOfExpandedMessagesUidsBackup.toMutableSet() + isThemeTheSameMap = threadBackup.isThemeTheSameMapBackup } else { thread.messages.forEachIndexed { index, message -> isExpandedMap[message.uid] = message.shouldBeExpanded(index, thread.messages.lastIndex).also { From 55a1f5f6e5118bb69e852ffce60dee723f2a3e54 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Tue, 27 Feb 2024 09:35:14 +0100 Subject: [PATCH 05/10] Backup if SuperCollapsedBlock has been clicked or not --- app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt | 3 +++ .../java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt | 1 + .../java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt | 1 + 3 files changed, 5 insertions(+) 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 f43c37338d..7605ab3193 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt @@ -213,17 +213,20 @@ class MainViewModel @Inject constructor( val isExpandedMapBackup: MutableMap, val initialSetOfExpandedMessagesUidsBackup: Set, val isThemeTheSameMapBackup: MutableMap, + val hasSuperCollapsedBlockBeenClicked: Boolean, ) fun createThreadBackup( isExpandedMap: MutableMap, initialSetOfExpandedMessagesUids: Set, isThemeTheSameMap: MutableMap, + hasSuperCollapsedBlockBeenClicked: Boolean, ) { threadBackup = ThreadBackup( isExpandedMap, initialSetOfExpandedMessagesUids, isThemeTheSameMap, + hasSuperCollapsedBlockBeenClicked, ) } 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 7b09a57d51..0f82bde26f 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 @@ -159,6 +159,7 @@ class ThreadFragment : Fragment() { isExpandedMap = isExpandedMap, initialSetOfExpandedMessagesUids = isExpandedMap.filter { it.value }.keys.toMutableSet(), isThemeTheSameMap = isThemeTheSameMap, + hasSuperCollapsedBlockBeenClicked = threadViewModel.superCollapsedBlock?.hasBeenClicked ?: false, ) super.onStop() diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt index 30eb614872..c99ba567ff 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt @@ -261,6 +261,7 @@ class ThreadViewModel @Inject constructor( isExpandedMap = threadBackup.isExpandedMapBackup initialSetOfExpandedMessagesUids = threadBackup.initialSetOfExpandedMessagesUidsBackup.toMutableSet() isThemeTheSameMap = threadBackup.isThemeTheSameMapBackup + if (threadBackup.hasSuperCollapsedBlockBeenClicked) superCollapsedBlock = SuperCollapsedBlock(hasBeenClicked = true) } else { thread.messages.forEachIndexed { index, message -> isExpandedMap[message.uid] = message.shouldBeExpanded(index, thread.messages.lastIndex).also { From a70c1027381c15f59d8c291bdd10601066a6122b Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Tue, 27 Feb 2024 10:43:24 +0100 Subject: [PATCH 06/10] Backup vertical scroll --- .../com/infomaniak/mail/ui/MainViewModel.kt | 3 ++ .../mail/ui/main/thread/ThreadFragment.kt | 46 ++++++++++--------- 2 files changed, 28 insertions(+), 21 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 7605ab3193..7d577bfa8a 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt @@ -214,6 +214,7 @@ class MainViewModel @Inject constructor( val initialSetOfExpandedMessagesUidsBackup: Set, val isThemeTheSameMapBackup: MutableMap, val hasSuperCollapsedBlockBeenClicked: Boolean, + val verticalScroll: Int, ) fun createThreadBackup( @@ -221,12 +222,14 @@ class MainViewModel @Inject constructor( initialSetOfExpandedMessagesUids: Set, isThemeTheSameMap: MutableMap, hasSuperCollapsedBlockBeenClicked: Boolean, + verticalScroll: Int, ) { threadBackup = ThreadBackup( isExpandedMap, initialSetOfExpandedMessagesUids, isThemeTheSameMap, hasSuperCollapsedBlockBeenClicked, + verticalScroll, ) } 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 0f82bde26f..95ecc83c93 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 @@ -160,6 +160,7 @@ class ThreadFragment : Fragment() { initialSetOfExpandedMessagesUids = isExpandedMap.filter { it.value }.keys.toMutableSet(), isThemeTheSameMap = isThemeTheSameMap, hasSuperCollapsedBlockBeenClicked = threadViewModel.superCollapsedBlock?.hasBeenClicked ?: false, + verticalScroll = binding.messagesListNestedScrollView.scrollY, ) super.onStop() @@ -578,32 +579,35 @@ class ThreadFragment : Fragment() { private fun scrollToFirstUnseenMessage() = with(binding) { - fun scrollToBottom() { - messagesListNestedScrollView.scrollY = messagesListNestedScrollView.maxScrollAmount - } + fun getBottomY(): Int = messagesListNestedScrollView.maxScrollAmount - val indexToScroll = threadAdapter.items.indexOfFirst { it is Message && threadAdapter.isExpandedMap[it.uid] == true } + val scrollY = mainViewModel.threadBackup?.verticalScroll ?: run { - // If no Message is expanded (e.g. the last Message of the Thread is a Draft), - // we want to automatically scroll to the very bottom. - if (indexToScroll == -1) { - scrollToBottom() - } else { - val targetChild = messagesList.getChildAt(indexToScroll) - if (targetChild == null) { - Sentry.withScope { scope -> - scope.level = SentryLevel.WARNING - scope.setExtra("indexToScroll", indexToScroll.toString()) - scope.setExtra("messageCount", threadAdapter.items.count().toString()) - scope.setExtra("isExpandedMap", threadAdapter.isExpandedMap.toString()) - scope.setExtra("isLastMessageDraft", (threadAdapter.items.lastOrNull() as Message?)?.isDraft.toString()) - Sentry.captureMessage("Target child for scroll in ThreadFragment is null. Fallback to scrolling to bottom") - } - scrollToBottom() + val indexToScroll = threadAdapter.items.indexOfFirst { it is Message && threadAdapter.isExpandedMap[it.uid] == true } + + // If no Message is expanded (e.g. the last Message of the Thread is a Draft), + // we want to automatically scroll to the very bottom. + if (indexToScroll == -1) { + getBottomY() } else { - messagesListNestedScrollView.scrollY = targetChild.top + val targetChild = messagesList.getChildAt(indexToScroll) + if (targetChild == null) { + Sentry.withScope { scope -> + scope.level = SentryLevel.ERROR + scope.setExtra("indexToScroll", indexToScroll.toString()) + scope.setExtra("messageCount", threadAdapter.items.count().toString()) + scope.setExtra("isExpandedMap", threadAdapter.isExpandedMap.toString()) + scope.setExtra("isLastMessageDraft", (threadAdapter.items.lastOrNull() as Message?)?.isDraft.toString()) + Sentry.captureMessage("Target child for scroll in ThreadFragment is null. Fallback to scrolling to bottom") + } + getBottomY() + } else { + targetChild.top + } } } + + messagesListNestedScrollView.scrollY = scrollY } private fun expandSuperCollapsedBlock() = with(threadViewModel) { From cc9556025d27a144843c2e430ce188b704fcd7e3 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Tue, 27 Feb 2024 13:34:07 +0100 Subject: [PATCH 07/10] Move ThreadBackup from MainVM to ThreadVM --- .../com/infomaniak/mail/ui/MainViewModel.kt | 32 ------------------ .../mail/ui/main/folder/TwoPaneFragment.kt | 4 +-- .../mail/ui/main/thread/ThreadAdapter.kt | 2 +- .../mail/ui/main/thread/ThreadFragment.kt | 10 +++--- .../mail/ui/main/thread/ThreadViewModel.kt | 33 ++++++++++++++++++- 5 files changed, 40 insertions(+), 41 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 7d577bfa8a..245710db93 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt @@ -206,38 +206,6 @@ class MainViewModel @Inject constructor( val mergedContactsLive: LiveData = avatarMergedContactData.mergedContactLiveData //endregion - //region Restore Thread state after going to MoveFragment or somewhere else, and then coming back to ThreadFragment. - var threadBackup: ThreadBackup? = null - - data class ThreadBackup( - val isExpandedMapBackup: MutableMap, - val initialSetOfExpandedMessagesUidsBackup: Set, - val isThemeTheSameMapBackup: MutableMap, - val hasSuperCollapsedBlockBeenClicked: Boolean, - val verticalScroll: Int, - ) - - fun createThreadBackup( - isExpandedMap: MutableMap, - initialSetOfExpandedMessagesUids: Set, - isThemeTheSameMap: MutableMap, - hasSuperCollapsedBlockBeenClicked: Boolean, - verticalScroll: Int, - ) { - threadBackup = ThreadBackup( - isExpandedMap, - initialSetOfExpandedMessagesUids, - isThemeTheSameMap, - hasSuperCollapsedBlockBeenClicked, - verticalScroll, - ) - } - - fun deleteThreadBackup() { - threadBackup = null - } - //endregion - fun updateUserInfo() = viewModelScope.launch(ioCoroutineContext) { SentryLog.d(TAG, "Update user info") updateAddressBooks() diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index 550756dd33..b1a742a147 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -45,7 +45,7 @@ import javax.inject.Inject abstract class TwoPaneFragment : Fragment() { val mainViewModel: MainViewModel by activityViewModels() - protected val twoPaneViewModel: TwoPaneViewModel by activityViewModels() + private val twoPaneViewModel: TwoPaneViewModel by activityViewModels() // TODO: When we'll update DragDropSwipeRecyclerViewLib, we'll need to make the adapter nullable. // For now it causes a memory leak, because we can't remove the strong reference @@ -143,7 +143,7 @@ abstract class TwoPaneFragment : Fragment() { } fun openThreadAndDeleteBackup(threadUid: String) { - mainViewModel.deleteThreadBackup() + getRightPane()?.getFragment()?.threadViewModel?.deleteThreadBackup() twoPaneViewModel.openThread(threadUid) } 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 aa90057780..47eed7278b 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 @@ -48,8 +48,8 @@ import com.infomaniak.mail.data.models.correspondent.Recipient import com.infomaniak.mail.data.models.message.Message import com.infomaniak.mail.data.models.message.Message.* import com.infomaniak.mail.databinding.* -import com.infomaniak.mail.ui.MainViewModel.ThreadBackup import com.infomaniak.mail.ui.main.thread.ThreadAdapter.* +import com.infomaniak.mail.ui.main.thread.ThreadViewModel.ThreadBackup import com.infomaniak.mail.utils.* import com.infomaniak.mail.utils.MailDateFormatUtils.mailFormattedDate import com.infomaniak.mail.utils.MailDateFormatUtils.mostDetailedDate 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 95ecc83c93..be6eb390ed 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 @@ -118,7 +118,7 @@ class ThreadFragment : Fragment() { private val mainViewModel: MainViewModel by activityViewModels() private val twoPaneViewModel: TwoPaneViewModel by activityViewModels() - private val threadViewModel: ThreadViewModel by viewModels() + val threadViewModel: ThreadViewModel by viewModels() private val twoPaneFragment inline get() = parentFragment as TwoPaneFragment private val threadAdapter inline get() = binding.messagesList.adapter as ThreadAdapter @@ -155,7 +155,7 @@ class ThreadFragment : Fragment() { override fun onStop() = with(threadAdapter) { - mainViewModel.createThreadBackup( + threadViewModel.createThreadBackup( isExpandedMap = isExpandedMap, initialSetOfExpandedMessagesUids = isExpandedMap.filter { it.value }.keys.toMutableSet(), isThemeTheSameMap = isThemeTheSameMap, @@ -315,7 +315,7 @@ class ThreadFragment : Fragment() { recycledViewPool.setMaxRecycledViews(0, 0) threadAdapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY - mainViewModel.threadBackup?.let(threadAdapter::useThreadBackup) + threadViewModel.threadBackup?.let(threadAdapter::useThreadBackup) } private fun setupDialogs() { @@ -332,7 +332,7 @@ class ThreadFragment : Fragment() { resetMessagesRelatedCache() displayThreadView() - openThread(threadUid, mainViewModel.threadBackup).observe(viewLifecycleOwner) { result -> + openThread(threadUid, threadBackup).observe(viewLifecycleOwner) { result -> if (result == null) { twoPaneViewModel.closeThread() @@ -581,7 +581,7 @@ class ThreadFragment : Fragment() { fun getBottomY(): Int = messagesListNestedScrollView.maxScrollAmount - val scrollY = mainViewModel.threadBackup?.verticalScroll ?: run { + val scrollY = threadViewModel.threadBackup?.verticalScroll ?: run { val indexToScroll = threadAdapter.items.indexOfFirst { it is Message && threadAdapter.isExpandedMap[it.uid] == true } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt index c99ba567ff..9a55947b1b 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt @@ -34,7 +34,6 @@ import com.infomaniak.mail.data.models.mailbox.Mailbox import com.infomaniak.mail.data.models.message.Message import com.infomaniak.mail.data.models.thread.Thread import com.infomaniak.mail.di.IoDispatcher -import com.infomaniak.mail.ui.MainViewModel.ThreadBackup import com.infomaniak.mail.ui.main.thread.ThreadAdapter.SuperCollapsedBlock import com.infomaniak.mail.utils.* import com.infomaniak.mail.utils.MessageBodyUtils.SplitBody @@ -93,6 +92,38 @@ class ThreadViewModel @Inject constructor( var superCollapsedBlock: SuperCollapsedBlock? = null + //region Restore Thread state after going to MoveFragment or somewhere else, and then coming back to ThreadFragment. + var threadBackup: ThreadBackup? = null + + data class ThreadBackup( + val isExpandedMapBackup: MutableMap, + val initialSetOfExpandedMessagesUidsBackup: Set, + val isThemeTheSameMapBackup: MutableMap, + val hasSuperCollapsedBlockBeenClicked: Boolean, + val verticalScroll: Int, + ) + + fun createThreadBackup( + isExpandedMap: MutableMap, + initialSetOfExpandedMessagesUids: Set, + isThemeTheSameMap: MutableMap, + hasSuperCollapsedBlockBeenClicked: Boolean, + verticalScroll: Int, + ) { + threadBackup = ThreadBackup( + isExpandedMap, + initialSetOfExpandedMessagesUids, + isThemeTheSameMap, + hasSuperCollapsedBlockBeenClicked, + verticalScroll, + ) + } + + fun deleteThreadBackup() { + threadBackup = null + } + //endregion + private val mailbox by lazy { mailboxController.getMailbox(AccountUtils.currentUserId, AccountUtils.currentMailboxId)!! } private val currentMailboxLive = mailboxController.getMailboxAsync( From a13c2472a5e2dded823d00e7888dc093994c3f46 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 28 Feb 2024 08:08:22 +0100 Subject: [PATCH 08/10] Use an interface to access Thread backup data from both VM & Adapter --- .../mail/ui/main/folder/TwoPaneFragment.kt | 2 +- .../mail/ui/main/thread/PrintMailFragment.kt | 10 ++- .../mail/ui/main/thread/ThreadAdapter.kt | 36 ++++------ .../mail/ui/main/thread/ThreadAdapterState.kt | 25 +++++++ .../mail/ui/main/thread/ThreadFragment.kt | 54 +++++--------- .../mail/ui/main/thread/ThreadViewModel.kt | 71 +++++-------------- 6 files changed, 84 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapterState.kt diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index b1a742a147..1f2c9f13db 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -143,7 +143,7 @@ abstract class TwoPaneFragment : Fragment() { } fun openThreadAndDeleteBackup(threadUid: String) { - getRightPane()?.getFragment()?.threadViewModel?.deleteThreadBackup() + getRightPane()?.getFragment()?.threadViewModel?.resetThreadBackupCache() twoPaneViewModel.openThread(threadUid) } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/PrintMailFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/PrintMailFragment.kt index 46edb5c985..a2f7efc2d2 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/PrintMailFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/PrintMailFragment.kt @@ -67,10 +67,16 @@ class PrintMailFragment : Fragment() { } } - private fun setupAdapter() = with(binding.messagesList) { - adapter = ThreadAdapter( + private fun setupAdapter() { + binding.messagesList.adapter = ThreadAdapter( shouldLoadDistantResources = true, isForPrinting = true, + threadAdapterState = object : ThreadAdapterState { + override var isExpandedMap by threadViewModel::isExpandedMap + override var isThemeTheSameMap by threadViewModel::isThemeTheSameMap + override var hasSuperCollapsedBlockBeenClicked by threadViewModel::hasSuperCollapsedBlockBeenClicked + override var verticalScroll by threadViewModel::verticalScroll + }, threadAdapterCallbacks = ThreadAdapterCallbacks( onBodyWebViewFinishedLoading = { startPrintingView() }, ), 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 47eed7278b..2578eba43a 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 @@ -49,7 +49,6 @@ import com.infomaniak.mail.data.models.message.Message import com.infomaniak.mail.data.models.message.Message.* import com.infomaniak.mail.databinding.* import com.infomaniak.mail.ui.main.thread.ThreadAdapter.* -import com.infomaniak.mail.ui.main.thread.ThreadViewModel.ThreadBackup import com.infomaniak.mail.utils.* import com.infomaniak.mail.utils.MailDateFormatUtils.mailFormattedDate import com.infomaniak.mail.utils.MailDateFormatUtils.mostDetailedDate @@ -72,21 +71,18 @@ class ThreadAdapter( private val shouldLoadDistantResources: Boolean, private val isForPrinting: Boolean = false, private val isCalendarEventExpandedMap: MutableMap = mutableMapOf(), + private val threadAdapterState: ThreadAdapterState, private var threadAdapterCallbacks: ThreadAdapterCallbacks? = null, ) : ListAdapter(MessageDiffCallback()) { inline val items: MutableList get() = currentList - var isExpandedMap = mutableMapOf() - //region Auto-scroll at Thread opening - var initialSetOfExpandedMessagesUids = setOf() private val currentSetOfLoadedExpandedMessagesUids = mutableSetOf() private var hasNotScrolledYet = true //endregion private val manuallyAllowedMessageUids = mutableSetOf() - var isThemeTheSameMap = mutableMapOf() private lateinit var recyclerView: RecyclerView private val webViewUtils by lazy { WebViewUtils(recyclerView.context) } @@ -151,7 +147,7 @@ class ThreadAdapter( } }.getOrDefault(Unit) - private fun MessageViewHolder.handleToggleLightModePayload(messageUid: String) { + private fun MessageViewHolder.handleToggleLightModePayload(messageUid: String) = with(threadAdapterState) { isThemeTheSameMap[messageUid] = !isThemeTheSameMap[messageUid]!! toggleContentAndQuoteTheme(messageUid) } @@ -159,7 +155,7 @@ class ThreadAdapter( private fun ItemMessageBinding.handleFailedMessagePayload(messageUid: String) { messageLoader.isGone = true failedLoadingErrorMessage.isVisible = true - if (isExpandedMap[messageUid] == true) onExpandedMessageLoaded(messageUid) + if (threadAdapterState.isExpandedMap[messageUid] == true) onExpandedMessageLoaded(messageUid) } private fun ItemMessageBinding.handleCalendarAttendancePayload(message: Message) { @@ -195,6 +191,7 @@ class ThreadAdapter( } private fun MessageViewHolder.bindMail(message: Message, position: Int) { + initMapForNewMessage(message, position) bindHeader(message) @@ -241,7 +238,7 @@ class ThreadAdapter( } } - private fun initMapForNewMessage(message: Message, position: Int) { + private fun initMapForNewMessage(message: Message, position: Int) = with(threadAdapterState) { if (isExpandedMap[message.uid] == null) { isExpandedMap[message.uid] = message.shouldBeExpanded(position, items.lastIndex) } @@ -250,7 +247,7 @@ class ThreadAdapter( } private fun MessageViewHolder.toggleContentAndQuoteTheme(messageUid: String) = with(binding) { - val isThemeTheSame = isThemeTheSameMap[messageUid]!! + val isThemeTheSame = threadAdapterState.isThemeTheSameMap[messageUid]!! bodyWebView.toggleWebViewTheme(isThemeTheSame) fullMessageWebView.toggleWebViewTheme(isThemeTheSame) toggleFrameLayoutsTheme(isThemeTheSame) @@ -286,7 +283,7 @@ class ThreadAdapter( private fun WebView.applyWebViewContent(uid: String, bodyWebView: String, type: String) { if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { - WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, isThemeTheSameMap[uid]!!) + WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, threadAdapterState.isThemeTheSameMap[uid]!!) } var styledBody = if (type == TEXT_PLAIN) createHtmlForPlainText(bodyWebView) else bodyWebView @@ -299,7 +296,8 @@ class ThreadAdapter( } private fun WebView.processMailDisplay(styledBody: String, uid: String, isForPrinting: Boolean): String { - val isDisplayedInDark = context.isNightModeEnabled() && isThemeTheSameMap[uid] == true && !isForPrinting + val isDisplayedInDark = + context.isNightModeEnabled() && threadAdapterState.isThemeTheSameMap[uid] == true && !isForPrinting return if (isForPrinting) { webViewUtils.processHtmlForPrint(styledBody, HtmlFormatter.PrintData(context, items.first() as Message)) } else { @@ -370,8 +368,8 @@ class ThreadAdapter( bccGroup.isVisible = message.bcc.isNotEmpty() } - private fun MessageViewHolder.handleHeaderClick(message: Message) = with(binding) { - messageHeader.setOnClickListener { + private fun MessageViewHolder.handleHeaderClick(message: Message) = with(threadAdapterState) { + binding.messageHeader.setOnClickListener { if (isExpandedMap[message.uid] == true) { isExpandedMap[message.uid] = false onExpandOrCollapseMessage(message) @@ -468,6 +466,7 @@ class ThreadAdapter( } private fun MessageViewHolder.bindBody(message: Message, hasQuote: Boolean) = with(binding) { + bodyWebView.setupLinkContextualMenu { data, type -> threadAdapterCallbacks?.promptLink?.invoke(data, type) } @@ -537,7 +536,7 @@ class ThreadAdapter( private fun onExpandedMessageLoaded(messageUid: String) { if (hasNotScrolledYet) { currentSetOfLoadedExpandedMessagesUids.add(messageUid) - if (currentSetOfLoadedExpandedMessagesUids.containsAll(initialSetOfExpandedMessagesUids)) { + if (currentSetOfLoadedExpandedMessagesUids.containsAll(threadAdapterState.isExpandedMap.keys)) { hasNotScrolledYet = false threadAdapterCallbacks?.onAllExpandedMessagesLoaded?.invoke() } @@ -545,7 +544,7 @@ class ThreadAdapter( } private fun MessageViewHolder.onExpandOrCollapseMessage(message: Message, shouldTrack: Boolean = true) = with(binding) { - val isExpanded = isExpandedMap[message.uid]!! + val isExpanded = threadAdapterState.isExpandedMap[message.uid]!! if (shouldTrack) context.trackMessageEvent("openMessage", isExpanded) @@ -604,12 +603,6 @@ class ThreadAdapter( indexOfMessage?.let { notifyItemChanged(it, NotifyType.ONLY_REBIND_CALENDAR_ATTENDANCE) } } - fun useThreadBackup(threadBackup: ThreadBackup) = with(threadBackup) { - isExpandedMap = isExpandedMapBackup - initialSetOfExpandedMessagesUids = initialSetOfExpandedMessagesUidsBackup - isThemeTheSameMap = isThemeTheSameMapBackup - } - private enum class NotifyType { TOGGLE_LIGHT_MODE, RE_RENDER, @@ -700,7 +693,6 @@ class ThreadAdapter( data class SuperCollapsedBlock( var shouldBeDisplayed: Boolean = true, - var hasBeenClicked: Boolean = false, val messagesUids: MutableSet = mutableSetOf(), ) { fun isFirstTime() = shouldBeDisplayed && messagesUids.isEmpty() diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapterState.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapterState.kt new file mode 100644 index 0000000000..62616b50c7 --- /dev/null +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapterState.kt @@ -0,0 +1,25 @@ +/* + * 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.main.thread + +interface ThreadAdapterState { + var isExpandedMap: MutableMap + var isThemeTheSameMap: MutableMap + var hasSuperCollapsedBlockBeenClicked: Boolean + var verticalScroll: 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 be6eb390ed..c9000a74d8 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 @@ -64,7 +64,6 @@ import com.infomaniak.mail.ui.main.folder.TwoPaneViewModel import com.infomaniak.mail.ui.main.folder.TwoPaneViewModel.NavData import com.infomaniak.mail.ui.main.thread.ThreadAdapter.ContextMenuType import com.infomaniak.mail.ui.main.thread.ThreadAdapter.ThreadAdapterCallbacks -import com.infomaniak.mail.ui.main.thread.ThreadViewModel.OpenThreadResult import com.infomaniak.mail.ui.main.thread.actions.AttachmentActionsBottomSheetDialogArgs import com.infomaniak.mail.ui.main.thread.actions.MessageActionsBottomSheetDialogArgs import com.infomaniak.mail.ui.main.thread.actions.ReplyBottomSheetDialogArgs @@ -153,19 +152,6 @@ class ThreadFragment : Fragment() { updateNavigationIcon() } - override fun onStop() = with(threadAdapter) { - - threadViewModel.createThreadBackup( - isExpandedMap = isExpandedMap, - initialSetOfExpandedMessagesUids = isExpandedMap.filter { it.value }.keys.toMutableSet(), - isThemeTheSameMap = isThemeTheSameMap, - hasSuperCollapsedBlockBeenClicked = threadViewModel.superCollapsedBlock?.hasBeenClicked ?: false, - verticalScroll = binding.messagesListNestedScrollView.scrollY, - ) - - super.onStop() - } - override fun onDestroyView() { threadAdapter.resetCallbacks() super.onDestroyView() @@ -215,6 +201,12 @@ class ThreadFragment : Fragment() { adapter = ThreadAdapter( shouldLoadDistantResources = shouldLoadDistantResources(), isCalendarEventExpandedMap = threadViewModel.isCalendarEventExpandedMap, + threadAdapterState = object : ThreadAdapterState { + override var isExpandedMap by threadViewModel::isExpandedMap + override var isThemeTheSameMap by threadViewModel::isThemeTheSameMap + override var hasSuperCollapsedBlockBeenClicked by threadViewModel::hasSuperCollapsedBlockBeenClicked + override var verticalScroll by threadViewModel::verticalScroll + }, threadAdapterCallbacks = ThreadAdapterCallbacks( onContactClicked = { safeNavigate( @@ -315,7 +307,6 @@ class ThreadFragment : Fragment() { recycledViewPool.setMaxRecycledViews(0, 0) threadAdapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY - threadViewModel.threadBackup?.let(threadAdapter::useThreadBackup) } private fun setupDialogs() { @@ -332,15 +323,14 @@ class ThreadFragment : Fragment() { resetMessagesRelatedCache() displayThreadView() - openThread(threadUid, threadBackup).observe(viewLifecycleOwner) { result -> + openThread(threadUid).observe(viewLifecycleOwner) { thread -> - if (result == null) { + if (thread == null) { twoPaneViewModel.closeThread() return@observe } - initUi(threadUid, folderRole = mainViewModel.getActionFolderRole(result.thread)) - initAdapter(result) + initUi(threadUid, folderRole = mainViewModel.getActionFolderRole(thread)) reassignThreadLive(threadUid) reassignMessagesLive(threadUid) @@ -517,14 +507,6 @@ class ThreadFragment : Fragment() { } } - private fun initAdapter(result: OpenThreadResult) { - threadAdapter.apply { - isExpandedMap = result.isExpandedMap - initialSetOfExpandedMessagesUids = result.initialSetOfExpandedMessagesUids - isThemeTheSameMap = result.isThemeTheSameMap - } - } - private fun scheduleDownloadManager(downloadUrl: String, filename: String) { fun scheduleDownloadManager() = mainViewModel.scheduleDownload(downloadUrl, filename) @@ -571,32 +553,32 @@ class ThreadFragment : Fragment() { args = MessageActionsBottomSheetDialogArgs( messageUid = uid, threadUid = twoPaneViewModel.currentThreadUid.value ?: return, - isThemeTheSame = threadAdapter.isThemeTheSameMap[uid] ?: return, + isThemeTheSame = threadViewModel.isThemeTheSameMap[uid] ?: return, shouldLoadDistantResources = shouldLoadDistantResources(uid), ).toBundle(), ) } - private fun scrollToFirstUnseenMessage() = with(binding) { + private fun scrollToFirstUnseenMessage() = with(threadViewModel) { - fun getBottomY(): Int = messagesListNestedScrollView.maxScrollAmount + fun getBottomY(): Int = binding.messagesListNestedScrollView.maxScrollAmount - val scrollY = threadViewModel.threadBackup?.verticalScroll ?: run { + val scrollY = verticalScroll ?: run { - val indexToScroll = threadAdapter.items.indexOfFirst { it is Message && threadAdapter.isExpandedMap[it.uid] == true } + val indexToScroll = threadAdapter.items.indexOfFirst { it is Message && isExpandedMap[it.uid] == true } // If no Message is expanded (e.g. the last Message of the Thread is a Draft), // we want to automatically scroll to the very bottom. if (indexToScroll == -1) { getBottomY() } else { - val targetChild = messagesList.getChildAt(indexToScroll) + val targetChild = binding.messagesList.getChildAt(indexToScroll) if (targetChild == null) { Sentry.withScope { scope -> scope.level = SentryLevel.ERROR scope.setExtra("indexToScroll", indexToScroll.toString()) scope.setExtra("messageCount", threadAdapter.items.count().toString()) - scope.setExtra("isExpandedMap", threadAdapter.isExpandedMap.toString()) + scope.setExtra("isExpandedMap", isExpandedMap.toString()) scope.setExtra("isLastMessageDraft", (threadAdapter.items.lastOrNull() as Message?)?.isDraft.toString()) Sentry.captureMessage("Target child for scroll in ThreadFragment is null. Fallback to scrolling to bottom") } @@ -607,11 +589,11 @@ class ThreadFragment : Fragment() { } } - messagesListNestedScrollView.scrollY = scrollY + binding.messagesListNestedScrollView.scrollY = scrollY } private fun expandSuperCollapsedBlock() = with(threadViewModel) { - superCollapsedBlock?.hasBeenClicked = true + hasSuperCollapsedBlockBeenClicked = true reassignMessagesLive(twoPaneViewModel.currentThreadUid.value!!) } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt index 9a55947b1b..e08c4c5262 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt @@ -90,38 +90,13 @@ class ThreadViewModel @Inject constructor( var shouldMarkThreadAsSeen: Boolean = false - var superCollapsedBlock: SuperCollapsedBlock? = null + private var superCollapsedBlock: SuperCollapsedBlock? = null //region Restore Thread state after going to MoveFragment or somewhere else, and then coming back to ThreadFragment. - var threadBackup: ThreadBackup? = null - - data class ThreadBackup( - val isExpandedMapBackup: MutableMap, - val initialSetOfExpandedMessagesUidsBackup: Set, - val isThemeTheSameMapBackup: MutableMap, - val hasSuperCollapsedBlockBeenClicked: Boolean, - val verticalScroll: Int, - ) - - fun createThreadBackup( - isExpandedMap: MutableMap, - initialSetOfExpandedMessagesUids: Set, - isThemeTheSameMap: MutableMap, - hasSuperCollapsedBlockBeenClicked: Boolean, - verticalScroll: Int, - ) { - threadBackup = ThreadBackup( - isExpandedMap, - initialSetOfExpandedMessagesUids, - isThemeTheSameMap, - hasSuperCollapsedBlockBeenClicked, - verticalScroll, - ) - } - - fun deleteThreadBackup() { - threadBackup = null - } + var isExpandedMap: MutableMap = mutableMapOf() + var isThemeTheSameMap: MutableMap = mutableMapOf() + var hasSuperCollapsedBlockBeenClicked: Boolean = false + var verticalScroll: Int? = null //endregion private val mailbox by lazy { mailboxController.getMailbox(AccountUtils.currentUserId, AccountUtils.currentMailboxId)!! } @@ -131,6 +106,13 @@ class ThreadViewModel @Inject constructor( AccountUtils.currentMailboxId, ).map { it.obj }.asLiveData(ioCoroutineContext) + fun resetThreadBackupCache() { + isExpandedMap = mutableMapOf() + isThemeTheSameMap = mutableMapOf() + hasSuperCollapsedBlockBeenClicked = false + verticalScroll = null + } + fun resetMessagesRelatedCache() { treatedMessagesForCalendarEvent.clear() isCalendarEventExpandedMap.clear() @@ -256,7 +238,7 @@ class ThreadViewModel @Inject constructor( private fun shouldBlockBeDisplayed(messagesCount: Int, firstIndexAfterBlock: Int, withSuperCollapsedBlock: Boolean): Boolean { return withSuperCollapsedBlock && // When we want to print a mail, we need the full list of Messages superCollapsedBlock?.shouldBeDisplayed == true && // If the Block was hidden for any reason, we mustn't ever display it again - superCollapsedBlock?.hasBeenClicked == false && // Block hasn't been expanded by the user + !hasSuperCollapsedBlockBeenClicked && // Block hasn't been expanded by the user messagesCount >= SUPER_COLLAPSED_BLOCK_MINIMUM_MESSAGES_LIMIT && // At least 5 Messages in the Thread firstIndexAfterBlock >= SUPER_COLLAPSED_BLOCK_FIRST_INDEX_LIMIT // At least 2 Messages in the Block } @@ -275,7 +257,7 @@ class ThreadViewModel @Inject constructor( return@withContext message } - fun openThread(threadUid: String, threadBackup: ThreadBackup?) = liveData(ioCoroutineContext) { + fun openThread(threadUid: String) = liveData(ioCoroutineContext) { val thread = threadController.getThread(threadUid) ?: run { emit(null) @@ -284,27 +266,17 @@ class ThreadViewModel @Inject constructor( sendMatomoAndSentryAboutThreadMessagesCount(thread) - var isExpandedMap = mutableMapOf() - var initialSetOfExpandedMessagesUids = mutableSetOf() - var isThemeTheSameMap = mutableMapOf() - - if (threadBackup != null) { - isExpandedMap = threadBackup.isExpandedMapBackup - initialSetOfExpandedMessagesUids = threadBackup.initialSetOfExpandedMessagesUidsBackup.toMutableSet() - isThemeTheSameMap = threadBackup.isThemeTheSameMapBackup - if (threadBackup.hasSuperCollapsedBlockBeenClicked) superCollapsedBlock = SuperCollapsedBlock(hasBeenClicked = true) - } else { + // These 2 will always be empty or not all together at the same time. + if (isExpandedMap.isEmpty() || isThemeTheSameMap.isEmpty()) { thread.messages.forEachIndexed { index, message -> - isExpandedMap[message.uid] = message.shouldBeExpanded(index, thread.messages.lastIndex).also { - if (it) initialSetOfExpandedMessagesUids.add(message.uid) - } + isExpandedMap[message.uid] = message.shouldBeExpanded(index, thread.messages.lastIndex) isThemeTheSameMap[message.uid] = true } } shouldMarkThreadAsSeen = thread.unseenMessagesCount > 0 - emit(OpenThreadResult(thread, isExpandedMap, initialSetOfExpandedMessagesUids, isThemeTheSameMap)) + emit(thread) } fun markThreadAsSeen() = viewModelScope.launch(ioCoroutineContext) { @@ -459,13 +431,6 @@ class ThreadViewModel @Inject constructor( val mailbox: Mailbox?, ) - data class OpenThreadResult( - val thread: Thread, - val isExpandedMap: MutableMap, - val initialSetOfExpandedMessagesUids: Set, - val isThemeTheSameMap: MutableMap, - ) - data class QuickActionBarResult( val threadUid: String, val message: Message, From e24029532af76eba2816aff585df7d47b7fa6117 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Wed, 28 Feb 2024 08:12:18 +0100 Subject: [PATCH 09/10] Make ThreadViewModel private again --- .../infomaniak/mail/ui/main/folder/ThreadListFragment.kt | 2 +- .../com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt | 6 +++--- .../com/infomaniak/mail/ui/main/thread/ThreadFragment.kt | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) 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 7598fa2764..a3a48f1919 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 @@ -191,7 +191,7 @@ class ThreadListFragment : TwoPaneFragment(), SwipeRefreshLayout.OnRefreshListen } // If we are coming from a Notification, we need to navigate to ThreadFragment. - openThreadAndDeleteBackup(threadUid) + openThreadAndResetBackup(threadUid) } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index 1f2c9f13db..c762055203 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -138,12 +138,12 @@ abstract class TwoPaneFragment : Fragment() { trackNewMessageEvent(OPEN_FROM_DRAFT_NAME) twoPaneViewModel.openDraft(thread) } else { - openThreadAndDeleteBackup(thread.uid) + openThreadAndResetBackup(thread.uid) } } - fun openThreadAndDeleteBackup(threadUid: String) { - getRightPane()?.getFragment()?.threadViewModel?.resetThreadBackupCache() + fun openThreadAndResetBackup(threadUid: String) { + getRightPane()?.getFragment()?.resetThreadBackupCache() twoPaneViewModel.openThread(threadUid) } 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 c9000a74d8..a696f426d6 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 @@ -117,7 +117,7 @@ class ThreadFragment : Fragment() { private val mainViewModel: MainViewModel by activityViewModels() private val twoPaneViewModel: TwoPaneViewModel by activityViewModels() - val threadViewModel: ThreadViewModel by viewModels() + private val threadViewModel: ThreadViewModel by viewModels() private val twoPaneFragment inline get() = parentFragment as TwoPaneFragment private val threadAdapter inline get() = binding.messagesList.adapter as ThreadAdapter @@ -158,6 +158,10 @@ class ThreadFragment : Fragment() { _binding = null } + fun resetThreadBackupCache() { + threadViewModel.resetThreadBackupCache() + } + private fun setupUi() = with(binding) { updateNavigationIcon() From 26140ef811a93554587ac34296bdff4282fb6bd5 Mon Sep 17 00:00:00 2001 From: Kevin Boulongne Date: Fri, 1 Mar 2024 07:46:08 +0100 Subject: [PATCH 10/10] Move Realm access on background Thread --- .../mail/ui/main/folder/TwoPaneViewModel.kt | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) 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 759ee0d05d..c840b87b46 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 @@ -20,26 +20,30 @@ package com.infomaniak.mail.ui.main.folder import android.net.Uri import android.os.Bundle import androidx.annotation.IdRes -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel +import androidx.lifecycle.* import com.infomaniak.lib.core.utils.SingleLiveEvent import com.infomaniak.mail.data.cache.mailboxContent.DraftController import com.infomaniak.mail.data.models.draft.Draft.DraftMode import com.infomaniak.mail.data.models.message.Message import com.infomaniak.mail.data.models.thread.Thread +import com.infomaniak.mail.di.IoDispatcher import com.infomaniak.mail.ui.newMessage.NewMessageActivityArgs import com.infomaniak.mail.utils.Utils.runCatchingRealm +import com.infomaniak.mail.utils.coroutineContext import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class TwoPaneViewModel @Inject constructor( private val state: SavedStateHandle, private val draftController: DraftController, + @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { + private val ioCoroutineContext = viewModelScope.coroutineContext(ioDispatcher) + val currentThreadUid: LiveData = state.getLiveData(CURRENT_THREAD_UID_KEY) inline val isThreadOpen get() = currentThreadUid.value != null @@ -57,16 +61,18 @@ class TwoPaneViewModel @Inject constructor( state[CURRENT_THREAD_UID_KEY] = null } - fun openDraft(thread: Thread) { + fun openDraft(thread: Thread) = viewModelScope.launch(ioCoroutineContext) { navigateToSelectedDraft(thread.messages.single()) } private fun navigateToSelectedDraft(message: Message) = runCatchingRealm { - newMessageArgs.value = NewMessageActivityArgs( - arrivedFromExistingDraft = true, - draftLocalUuid = draftController.getDraftByMessageUid(message.uid)?.localUuid, - draftResource = message.draftResource, - messageUid = message.uid, + newMessageArgs.postValue( + NewMessageActivityArgs( + arrivedFromExistingDraft = true, + draftLocalUuid = draftController.getDraftByMessageUid(message.uid)?.localUuid, + draftResource = message.draftResource, + messageUid = message.uid, + ), ) }