From 6c921b784663cee7b02af81546cf5f9fb9957229 Mon Sep 17 00:00:00 2001 From: Rafael Tonholo Date: Tue, 6 Jan 2026 10:12:02 -0400 Subject: [PATCH 1/2] refactor(message-list): rename exposed view model to legacyViewModel --- .../src/main/java/com/fsck/k9/activity/MainActivity.kt | 4 ++-- .../k9/ui/messagelist/AbstractMessageListFragment.kt | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MainActivity.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MainActivity.kt index 1e4f38d94f7..b4e7a8897b6 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MainActivity.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MainActivity.kt @@ -273,7 +273,7 @@ open class MainActivity : fragmentManager.findFragmentByTag(FRAGMENT_TAG_MESSAGE_VIEW_CONTAINER) as? MessageViewContainerFragment messageListFragment?.let { messageListFragment -> - messageViewContainerFragment?.setViewModel(messageListFragment.viewModel) + messageViewContainerFragment?.setViewModel(messageListFragment.legacyViewModel) initializeFromLocalSearch(messageListFragment.localSearch) } } @@ -1110,7 +1110,7 @@ open class MainActivity : messageViewContainerFragment = fragment messageListFragment?.let { messageListFragment -> - fragment.setViewModel(messageListFragment.viewModel) + fragment.setViewModel(messageListFragment.legacyViewModel) } if (displayMode == DisplayMode.SPLIT_VIEW) { diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/AbstractMessageListFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/AbstractMessageListFragment.kt index 3bb8dcf5c0f..c0fcb1437e9 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/AbstractMessageListFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/AbstractMessageListFragment.kt @@ -147,7 +147,8 @@ abstract class AbstractMessageListFragment : abstract val logTag: String - val viewModel: MessageListViewModel by viewModel() + val legacyViewModel: MessageListViewModel by viewModel() + private val viewModel: MessageListViewModel get() = legacyViewModel private val recentChangesViewModel: RecentChangesViewModel by viewModel() private val generalSettingsManager: GeneralSettingsManager by inject() @@ -1187,7 +1188,7 @@ abstract class AbstractMessageListFragment : when (outcome.error) { is AuthDebugActions.Error.AccountNotFound, is AuthDebugActions.Error.NoOAuthState, - -> { + -> { Toast.makeText( requireContext(), R.string.debug_invalidate_access_token_unavailable, @@ -1240,7 +1241,7 @@ abstract class AbstractMessageListFragment : is AuthDebugActions.Error.NoOAuthState, is AuthDebugActions.Error.CannotModifyAccessToken, is AuthDebugActions.Error.AlreadyModified, - -> { + -> { Toast.makeText( requireContext(), R.string.debug_invalidate_access_token_unavailable, @@ -1282,7 +1283,7 @@ abstract class AbstractMessageListFragment : is AuthDebugActions.Error.CannotModifyAccessToken, is AuthDebugActions.Error.AlreadyModified, - -> { + -> { // Not relevant to this action, but keep exhaustive when; show generic unavailable Toast.makeText( requireContext(), From 9632bedda54c7a44eac39bbfcb7499068b204f8e Mon Sep 17 00:00:00 2001 From: Rafael Tonholo Date: Tue, 6 Jan 2026 10:42:26 -0400 Subject: [PATCH 2/2] feat(message-list): add `MessageListFragment` and MVI contract --- .../message/list/ui/MessageListContract.kt | 31 ++++++++++++++ .../list/internal/FeatureMessageListModule.kt | 6 +++ .../list/internal/ui/MessageListViewModel.kt | 10 +++++ .../main/java/com/fsck/k9/ui/KoinModule.kt | 11 ++++- .../AbstractMessageListFragment.kt | 13 +++--- .../k9/ui/messagelist/MessageListFragment.kt | 42 +++++++++++++++++++ 6 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 feature/mail/message/list/api/src/main/kotlin/net/thunderbird/feature/mail/message/list/ui/MessageListContract.kt create mode 100644 feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/ui/MessageListViewModel.kt create mode 100644 legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt diff --git a/feature/mail/message/list/api/src/main/kotlin/net/thunderbird/feature/mail/message/list/ui/MessageListContract.kt b/feature/mail/message/list/api/src/main/kotlin/net/thunderbird/feature/mail/message/list/ui/MessageListContract.kt new file mode 100644 index 00000000000..ac7450a4479 --- /dev/null +++ b/feature/mail/message/list/api/src/main/kotlin/net/thunderbird/feature/mail/message/list/ui/MessageListContract.kt @@ -0,0 +1,31 @@ +package net.thunderbird.feature.mail.message.list.ui + +import app.k9mail.core.ui.compose.common.mvi.BaseViewModel +import net.thunderbird.feature.mail.message.list.ui.effect.MessageListEffect +import net.thunderbird.feature.mail.message.list.ui.event.MessageListEvent +import net.thunderbird.feature.mail.message.list.ui.state.MessageListState + +/** + * Defines the contract between the View and the ViewModel for the message list screen. + * + * This contract follows the MVI (Model-View-Intent) pattern, specifying the structure for: + * - **State (`MessageListState`)**: Represents the UI state. + * - **Events (`MessageListEvent`)**: User actions or other events from the UI. + * - **Effects (`MessageListEffect`)**: One-time actions for the UI to perform (e.g., navigation, showing a toast). + */ +interface MessageListContract { + /** + * The view model for the message list screen. + * + * It is responsible for handling the business logic of the message list screen and for providing the + * [MessageListState] to the UI. It consumes [MessageListEvent]s and produces [MessageListEffect]s. + * + * @see BaseViewModel + * @see MessageListState + * @see MessageListEvent + * @see MessageListEffect + */ + abstract class ViewModel : BaseViewModel( + initialState = MessageListState.WarmingUp(), + ) +} diff --git a/feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/FeatureMessageListModule.kt b/feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/FeatureMessageListModule.kt index 1f133f0636b..d60b4becd7e 100644 --- a/feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/FeatureMessageListModule.kt +++ b/feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/FeatureMessageListModule.kt @@ -5,8 +5,10 @@ import net.thunderbird.feature.mail.message.list.internal.domain.usecase.BuildSw import net.thunderbird.feature.mail.message.list.internal.domain.usecase.CreateArchiveFolder import net.thunderbird.feature.mail.message.list.internal.domain.usecase.GetAccountFolders import net.thunderbird.feature.mail.message.list.internal.domain.usecase.SetArchiveFolder +import net.thunderbird.feature.mail.message.list.internal.ui.MessageListViewModel import net.thunderbird.feature.mail.message.list.internal.ui.dialog.SetupArchiveFolderDialogFragment import net.thunderbird.feature.mail.message.list.internal.ui.dialog.SetupArchiveFolderDialogViewModel +import net.thunderbird.feature.mail.message.list.ui.MessageListContract import net.thunderbird.feature.mail.message.list.ui.dialog.SetupArchiveFolderDialogContract import net.thunderbird.feature.mail.message.list.ui.dialog.SetupArchiveFolderDialogFragmentFactory import org.koin.core.module.dsl.viewModel @@ -47,4 +49,8 @@ val featureMessageListModule = module { ) as SetupArchiveFolderDialogContract.ViewModel } factory { SetupArchiveFolderDialogFragment.Factory } + + viewModel { parameters -> + MessageListViewModel() + } } diff --git a/feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/ui/MessageListViewModel.kt b/feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/ui/MessageListViewModel.kt new file mode 100644 index 00000000000..c011e085576 --- /dev/null +++ b/feature/mail/message/list/internal/src/main/kotlin/net/thunderbird/feature/mail/message/list/internal/ui/MessageListViewModel.kt @@ -0,0 +1,10 @@ +package net.thunderbird.feature.mail.message.list.internal.ui + +import net.thunderbird.feature.mail.message.list.ui.MessageListContract +import net.thunderbird.feature.mail.message.list.ui.event.MessageListEvent + +class MessageListViewModel : MessageListContract.ViewModel() { + override fun event(event: MessageListEvent) { + // TODO(9497): Handle events. + } +} diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/KoinModule.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/KoinModule.kt index 9d5474c67af..13f7039b8a5 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/KoinModule.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/KoinModule.kt @@ -7,10 +7,13 @@ import com.fsck.k9.ui.helper.DisplayHtmlUiFactory import com.fsck.k9.ui.helper.SizeFormatter import com.fsck.k9.ui.messagelist.AbstractMessageListFragment import com.fsck.k9.ui.messagelist.LegacyMessageListFragment +import com.fsck.k9.ui.messagelist.MessageListFragment import com.fsck.k9.ui.messageview.LinkTextHandler import com.fsck.k9.ui.settings.AboutViewModel import com.fsck.k9.ui.share.ShareIntentBuilder import net.thunderbird.core.common.inject.getList +import net.thunderbird.core.featureflag.FeatureFlagProvider +import net.thunderbird.feature.mail.message.list.MessageListFeatureFlags import org.koin.core.module.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module @@ -31,7 +34,11 @@ val uiModule = module { factory { ShareIntentBuilder(resourceProvider = get(), textPartFinder = get(), quoteDateFormatter = get()) } factory { LinkTextHandler(context = get(), clipboardManager = get()) } factory { - // TODO(9497): verify if EnableMessageListNewState is enabled. If so, use the new MessageListFragment instead. - LegacyMessageListFragment.Factory + val featureFlagProvider = get() + if (featureFlagProvider.provide(MessageListFeatureFlags.EnableMessageListNewState).isEnabled()) { + MessageListFragment.Factory + } else { + LegacyMessageListFragment.Factory + } } } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/AbstractMessageListFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/AbstractMessageListFragment.kt index c0fcb1437e9..135415822ea 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/AbstractMessageListFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/AbstractMessageListFragment.kt @@ -202,7 +202,8 @@ abstract class AbstractMessageListFragment : private lateinit var adapter: MessageListAdapter - private lateinit var accountUuids: Array + protected lateinit var accountUuids: Array + private set private var accounts: List = emptyList() private var account: LegacyAccount? = null @@ -334,7 +335,7 @@ abstract class AbstractMessageListFragment : rememberedSelected = savedInstanceState.getLongArray(STATE_SELECTED_MESSAGES)?.toSet() } - private fun decodeArguments(): Error? { + protected fun decodeArguments(): Error? { val arguments = requireArguments() showingThreadedList = arguments.getBoolean(ARG_THREADED_LIST, false) isThreadDisplay = arguments.getBoolean(ARG_IS_THREAD_DISPLAY, false) @@ -1188,7 +1189,7 @@ abstract class AbstractMessageListFragment : when (outcome.error) { is AuthDebugActions.Error.AccountNotFound, is AuthDebugActions.Error.NoOAuthState, - -> { + -> { Toast.makeText( requireContext(), R.string.debug_invalidate_access_token_unavailable, @@ -1241,7 +1242,7 @@ abstract class AbstractMessageListFragment : is AuthDebugActions.Error.NoOAuthState, is AuthDebugActions.Error.CannotModifyAccessToken, is AuthDebugActions.Error.AlreadyModified, - -> { + -> { Toast.makeText( requireContext(), R.string.debug_invalidate_access_token_unavailable, @@ -1283,7 +1284,7 @@ abstract class AbstractMessageListFragment : is AuthDebugActions.Error.CannotModifyAccessToken, is AuthDebugActions.Error.AlreadyModified, - -> { + -> { // Not relevant to this action, but keep exhaustive when; show generic unavailable Toast.makeText( requireContext(), @@ -2591,7 +2592,7 @@ abstract class AbstractMessageListFragment : } @Suppress("detekt.UnnecessaryAnnotationUseSiteTarget") // https://github.com/detekt/detekt/issues/8212 - private enum class Error(@param:StringRes val errorText: Int) { + protected enum class Error(@param:StringRes val errorText: Int) { FolderNotFound(R.string.message_list_error_folder_not_found), } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt new file mode 100644 index 00000000000..f1c7cdcd40f --- /dev/null +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt @@ -0,0 +1,42 @@ +package com.fsck.k9.ui.messagelist + +import androidx.core.os.bundleOf +import net.thunderbird.feature.account.AccountIdFactory +import net.thunderbird.feature.mail.message.list.ui.MessageListContract +import net.thunderbird.feature.search.legacy.LocalMessageSearch +import net.thunderbird.feature.search.legacy.serialization.LocalMessageSearchSerializer +import org.koin.android.ext.android.inject +import org.koin.core.parameter.parameterSetOf + +private const val TAG = "MessageListFragment" + +// TODO(10322): Move this fragment to :feature:mail:message:list once all migration to the new +// MessageListFragment to MVI is done. +class MessageListFragment : AbstractMessageListFragment() { + override val logTag: String = TAG + + // TODO(9497): Remove suppression once we start use the new view model. + @Suppress("UnusedPrivateProperty") + private val viewModel: MessageListContract.ViewModel by inject { + decodeArguments() + parameterSetOf(accountUuids.map { AccountIdFactory.of(it) }.toSet()) + } + + companion object Factory : AbstractMessageListFragment.Factory { + override fun newInstance( + search: LocalMessageSearch, + isThreadDisplay: Boolean, + threadedList: Boolean, + ): MessageListFragment { + val searchBytes = LocalMessageSearchSerializer.serialize(search) + + return MessageListFragment().apply { + arguments = bundleOf( + ARG_SEARCH to searchBytes, + ARG_IS_THREAD_DISPLAY to isThreadDisplay, + ARG_THREADED_LIST to threadedList, + ) + } + } + } +}