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..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. - twoPaneViewModel.openThread(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 f560fa4076..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 @@ -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 @@ -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) + openThreadAndResetBackup(thread.uid) } } + fun openThreadAndResetBackup(threadUid: String) { + getRightPane()?.getFragment()?.resetThreadBackupCache() + twoPaneViewModel.openThread(threadUid) + } + private fun resetPanes() { if (isOnlyLeftShown()) { 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, + ), ) } 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 dc8969172d..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 @@ -71,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) } @@ -150,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) } @@ -158,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) { @@ -194,6 +191,7 @@ class ThreadAdapter( } private fun MessageViewHolder.bindMail(message: Message, position: Int) { + initMapForNewMessage(message, position) bindHeader(message) @@ -240,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) } @@ -249,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) @@ -285,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 @@ -298,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 { @@ -369,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) @@ -467,6 +466,7 @@ class ThreadAdapter( } private fun MessageViewHolder.bindBody(message: Message, hasQuote: Boolean) = with(binding) { + bodyWebView.setupLinkContextualMenu { data, type -> threadAdapterCallbacks?.promptLink?.invoke(data, type) } @@ -536,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() } @@ -544,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) @@ -693,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 b9b1eb763d..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 @@ -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 @@ -159,6 +158,10 @@ class ThreadFragment : Fragment() { _binding = null } + fun resetThreadBackupCache() { + threadViewModel.resetThreadBackupCache() + } + private fun setupUi() = with(binding) { updateNavigationIcon() @@ -202,6 +205,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( @@ -318,15 +327,14 @@ class ThreadFragment : Fragment() { resetMessagesRelatedCache() displayThreadView() - openThread(threadUid).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) @@ -503,14 +511,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) @@ -557,44 +557,47 @@ 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 scrollToBottom() { - messagesListNestedScrollView.scrollY = messagesListNestedScrollView.maxScrollAmount - } + fun getBottomY(): Int = binding.messagesListNestedScrollView.maxScrollAmount - val indexToScroll = threadAdapter.items.indexOfFirst { it is Message && threadAdapter.isExpandedMap[it.uid] == true } + val scrollY = 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 && 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 = 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", 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 + } } } + + 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 700242c60e..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,7 +90,14 @@ 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 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)!! } @@ -99,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() @@ -224,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 } @@ -252,19 +266,17 @@ 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) + // 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) + isThemeTheSameMap[message.uid] = true } - isThemeTheSameMap[message.uid] = true } shouldMarkThreadAsSeen = thread.unseenMessagesCount > 0 - emit(OpenThreadResult(thread, isExpandedMap, initialSetOfExpandedMessagesUids, isThemeTheSameMap)) + emit(thread) } fun markThreadAsSeen() = viewModelScope.launch(ioCoroutineContext) { @@ -419,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,