diff --git a/app-k9mail/src/debug/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt b/app-k9mail/src/debug/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt index c1a641f83b2..abcc807ed3c 100644 --- a/app-k9mail/src/debug/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt +++ b/app-k9mail/src/debug/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt @@ -1,5 +1,6 @@ package app.k9mail.featureflag +import com.fsck.k9.ui.messagelist.MessageListFeatureFlags import net.thunderbird.core.featureflag.FeatureFlag import net.thunderbird.core.featureflag.FeatureFlagFactory import net.thunderbird.core.featureflag.FeatureFlagKey @@ -16,6 +17,7 @@ class K9FeatureFlagFactory : FeatureFlagFactory { FeatureFlag("enable_dropdown_drawer_ui".toFeatureFlagKey(), enabled = true), FeatureFlag(FeatureFlagKey.DisplayInAppNotifications, enabled = true), FeatureFlag(FeatureFlagKey.UseNotificationSenderForSystemNotifications, enabled = true), + FeatureFlag(MessageListFeatureFlags.UseComposeForMessageListItems, enabled = false), ) } } diff --git a/app-k9mail/src/release/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt b/app-k9mail/src/release/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt index 851dc1ac843..f380c538fa8 100644 --- a/app-k9mail/src/release/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt +++ b/app-k9mail/src/release/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt @@ -1,5 +1,6 @@ package app.k9mail.featureflag +import com.fsck.k9.ui.messagelist.MessageListFeatureFlags import net.thunderbird.core.featureflag.FeatureFlag import net.thunderbird.core.featureflag.FeatureFlagFactory import net.thunderbird.core.featureflag.FeatureFlagKey @@ -20,6 +21,7 @@ class K9FeatureFlagFactory : FeatureFlagFactory { FeatureFlag("enable_dropdown_drawer_ui".toFeatureFlagKey(), enabled = false), FeatureFlag(FeatureFlagKey.DisplayInAppNotifications, enabled = false), FeatureFlag(FeatureFlagKey.UseNotificationSenderForSystemNotifications, enabled = false), + FeatureFlag(MessageListFeatureFlags.UseComposeForMessageListItems, enabled = false), ) } } diff --git a/app-thunderbird/src/beta/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt b/app-thunderbird/src/beta/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt index 6f5a084d4e8..003a330767a 100644 --- a/app-thunderbird/src/beta/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt +++ b/app-thunderbird/src/beta/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt @@ -1,5 +1,6 @@ package net.thunderbird.android.featureflag +import com.fsck.k9.ui.messagelist.MessageListFeatureFlags import net.thunderbird.core.featureflag.FeatureFlag import net.thunderbird.core.featureflag.FeatureFlagFactory import net.thunderbird.core.featureflag.FeatureFlagKey @@ -19,6 +20,7 @@ class TbFeatureFlagFactory : FeatureFlagFactory { FeatureFlag("enable_dropdown_drawer_ui".toFeatureFlagKey(), enabled = true), FeatureFlag(FeatureFlagKey.DisplayInAppNotifications, enabled = true), FeatureFlag(FeatureFlagKey.UseNotificationSenderForSystemNotifications, enabled = false), + FeatureFlag(MessageListFeatureFlags.UseComposeForMessageListItems, enabled = false), ) } } diff --git a/app-thunderbird/src/daily/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt b/app-thunderbird/src/daily/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt index 47e9cab5c94..4d17004e6b5 100644 --- a/app-thunderbird/src/daily/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt +++ b/app-thunderbird/src/daily/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt @@ -1,5 +1,6 @@ package net.thunderbird.android.featureflag +import com.fsck.k9.ui.messagelist.MessageListFeatureFlags import net.thunderbird.core.featureflag.FeatureFlag import net.thunderbird.core.featureflag.FeatureFlagFactory import net.thunderbird.core.featureflag.FeatureFlagKey @@ -19,6 +20,7 @@ class TbFeatureFlagFactory : FeatureFlagFactory { FeatureFlag("enable_dropdown_drawer_ui".toFeatureFlagKey(), enabled = true), FeatureFlag(FeatureFlagKey.DisplayInAppNotifications, enabled = true), FeatureFlag(FeatureFlagKey.UseNotificationSenderForSystemNotifications, enabled = true), + FeatureFlag(MessageListFeatureFlags.UseComposeForMessageListItems, enabled = false), ) } } diff --git a/app-thunderbird/src/debug/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt b/app-thunderbird/src/debug/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt index e0591a363df..174a8ee9751 100644 --- a/app-thunderbird/src/debug/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt +++ b/app-thunderbird/src/debug/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt @@ -1,5 +1,6 @@ package net.thunderbird.android.featureflag +import com.fsck.k9.ui.messagelist.MessageListFeatureFlags import net.thunderbird.core.featureflag.FeatureFlag import net.thunderbird.core.featureflag.FeatureFlagFactory import net.thunderbird.core.featureflag.FeatureFlagKey @@ -19,6 +20,7 @@ class TbFeatureFlagFactory : FeatureFlagFactory { FeatureFlag("enable_dropdown_drawer_ui".toFeatureFlagKey(), enabled = true), FeatureFlag(FeatureFlagKey.DisplayInAppNotifications, enabled = true), FeatureFlag(FeatureFlagKey.UseNotificationSenderForSystemNotifications, enabled = true), + FeatureFlag(MessageListFeatureFlags.UseComposeForMessageListItems, enabled = false), ) } } diff --git a/app-thunderbird/src/release/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt b/app-thunderbird/src/release/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt index f79e5990d8d..86d68154e66 100644 --- a/app-thunderbird/src/release/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt +++ b/app-thunderbird/src/release/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt @@ -1,5 +1,6 @@ package net.thunderbird.android.featureflag +import com.fsck.k9.ui.messagelist.MessageListFeatureFlags import net.thunderbird.core.featureflag.FeatureFlag import net.thunderbird.core.featureflag.FeatureFlagFactory import net.thunderbird.core.featureflag.FeatureFlagKey @@ -19,6 +20,7 @@ class TbFeatureFlagFactory : FeatureFlagFactory { FeatureFlag("enable_dropdown_drawer_ui".toFeatureFlagKey(), enabled = false), FeatureFlag(FeatureFlagKey.DisplayInAppNotifications, enabled = false), FeatureFlag(FeatureFlagKey.UseNotificationSenderForSystemNotifications, enabled = false), + FeatureFlag(MessageListFeatureFlags.UseComposeForMessageListItems, enabled = false), ) } } diff --git a/legacy/ui/legacy/build.gradle.kts b/legacy/ui/legacy/build.gradle.kts index 5c44fd33075..6042f39e6e8 100644 --- a/legacy/ui/legacy/build.gradle.kts +++ b/legacy/ui/legacy/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.core.ui.theme.api) implementation(projects.feature.launcher) implementation(projects.core.common) + implementation(projects.core.ui.compose.designsystem) implementation(projects.feature.navigation.drawer.api) implementation(projects.feature.navigation.drawer.dropdown) implementation(projects.feature.navigation.drawer.siderail) diff --git a/legacy/ui/legacy/src/debug/kotlin/com/fsck/k9/ui/messagelist/item/MessageItemContentPreview.kt b/legacy/ui/legacy/src/debug/kotlin/com/fsck/k9/ui/messagelist/item/MessageItemContentPreview.kt new file mode 100644 index 00000000000..aae82769ac1 --- /dev/null +++ b/legacy/ui/legacy/src/debug/kotlin/com/fsck/k9/ui/messagelist/item/MessageItemContentPreview.kt @@ -0,0 +1,99 @@ +package com.fsck.k9.ui.messagelist.item + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewLightDark +import app.k9mail.core.ui.compose.designsystem.PreviewWithThemesLightDark +import com.fsck.k9.FontSizes +import com.fsck.k9.UiDensity +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.ConnectionSecurity +import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.ui.messagelist.MessageListAppearance +import com.fsck.k9.ui.messagelist.MessageListItem +import net.thunderbird.core.android.account.Identity +import net.thunderbird.core.android.account.LegacyAccount +import net.thunderbird.feature.account.AccountIdFactory +import net.thunderbird.feature.account.storage.profile.AvatarDto +import net.thunderbird.feature.account.storage.profile.AvatarTypeDto +import net.thunderbird.feature.account.storage.profile.ProfileDto + +@Composable +@PreviewLightDark +internal fun MessageItemContentPreview() { + PreviewWithThemesLightDark { + MessageItemContent( + item = fakeMessageListItem, + isActive = true, + isSelected = false, + onClick = {}, + onLongClick = {}, + onAvatarClick = {}, + onFavouriteClick = {}, + appearance = fakeMessageListAppearance, + ) + } +} + +private val accountId = AccountIdFactory.create() + +private val serverSettings = ServerSettings( + type = "imap", + host = "imap.example.com", + port = 993, + connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED, + authenticationType = AuthType.PLAIN, + username = "username", + password = "password", + clientCertificateAlias = null, +) +private val fakeMessageListItem = MessageListItem( + account = LegacyAccount( + id = accountId, + name = "Name", + email = "test@example.com", + profile = ProfileDto( + id = accountId, + name = "Name", + color = 0xFF0000FF.toInt(), + avatar = AvatarDto( + avatarType = AvatarTypeDto.MONOGRAM, + avatarMonogram = "AB", + avatarImageUri = null, + avatarIconName = null, + ), + ), + incomingServerSettings = serverSettings, + outgoingServerSettings = serverSettings, + identities = listOf(Identity()), + ), + subject = "Subject", + threadCount = 0, + messageDate = 1234456789L, + internalDate = 1234456789L, + displayName = "Sender Name", + displayAddress = null, + previewText = "This is the preview text.", + isMessageEncrypted = false, + isRead = false, + isStarred = false, + isAnswered = false, + isForwarded = false, + hasAttachments = false, + uniqueId = 42L, + folderId = 123L, + messageUid = "654321", + databaseId = 1L, + threadRoot = 1L, +) + +private val fakeMessageListAppearance = MessageListAppearance( + fontSizes = FontSizes(), + previewLines = 2, + stars = true, + senderAboveSubject = false, + showContactPicture = true, + showingThreadedList = false, + backGroundAsReadIndicator = false, + showAccountIndicator = true, + density = UiDensity.Default, +) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt index 838d6286d6d..f059eec8eb7 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt @@ -141,8 +141,8 @@ open class MessageList : private var messageListWasDisplayed = false private var viewSwitcher: ViewSwitcher? = null - private val isShowAccountChip: Boolean - get() = messageListFragment?.isShowAccountChip ?: true + private val isShowAccountIndicator: Boolean + get() = messageListFragment?.isShowAccountIndicator ?: true public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -1106,7 +1106,7 @@ open class MessageList : } else { val fragment = MessageViewContainerFragment.newInstance( reference = messageReference, - showAccountChip = isShowAccountChip, + isShowAccountIndicator = isShowAccountIndicator, ) supportFragmentManager.commitNow { replace(R.id.message_view_container, fragment, FRAGMENT_TAG_MESSAGE_VIEW_CONTAINER) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt index 4bc9b866037..c1221d8bddf 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt @@ -17,7 +17,9 @@ import app.k9mail.feature.launcher.FeatureLauncherTarget import app.k9mail.legacy.message.controller.MessageReference import com.fsck.k9.contacts.ContactPictureLoader import com.fsck.k9.ui.helper.RelativeDateTimeFormatter +import com.fsck.k9.ui.messagelist.MessageListFeatureFlags.UseComposeForMessageListItems import com.fsck.k9.ui.messagelist.item.BannerInlineListInAppNotificationViewHolder +import com.fsck.k9.ui.messagelist.item.ComposableMessageViewHolder import com.fsck.k9.ui.messagelist.item.FooterViewHolder import com.fsck.k9.ui.messagelist.item.MessageListViewHolder import com.fsck.k9.ui.messagelist.item.MessageViewHolder @@ -25,6 +27,7 @@ import com.fsck.k9.ui.messagelist.item.MessageViewHolderColors import net.thunderbird.core.featureflag.FeatureFlagKey import net.thunderbird.core.featureflag.FeatureFlagProvider import net.thunderbird.core.featureflag.FeatureFlagResult +import net.thunderbird.core.ui.theme.api.FeatureThemeProvider import net.thunderbird.feature.notification.api.ui.action.NotificationAction private const val FOOTER_ID = 1L @@ -42,6 +45,7 @@ class MessageListAdapter internal constructor( private val listItemListener: MessageListItemActionListener, private val appearance: MessageListAppearance, private val relativeDateTimeFormatter: RelativeDateTimeFormatter, + private val themeProvider: FeatureThemeProvider, private val featureFlagProvider: FeatureFlagProvider, ) : RecyclerView.Adapter() { @@ -227,7 +231,15 @@ class MessageListAdapter internal constructor( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageListViewHolder { return when (viewType) { - TYPE_MESSAGE -> createMessageViewHolder(parent) + TYPE_MESSAGE -> { + val result = featureFlagProvider.provide(UseComposeForMessageListItems) + if (result.isEnabled()) { + createComposableMessageViewHolder(parent) + } else { + createMessageViewHolder(parent) + } + } + TYPE_FOOTER -> FooterViewHolder.create(layoutInflater, parent, footerClickListener) TYPE_IN_APP_NOTIFICATION_BANNER_INLINE_LIST if isInAppNotificationEnabled -> BannerInlineListInAppNotificationViewHolder( @@ -259,7 +271,7 @@ class MessageListAdapter internal constructor( } } - private fun createMessageViewHolder(parent: ViewGroup?): MessageViewHolder = + private fun createMessageViewHolder(parent: ViewGroup): MessageViewHolder = MessageViewHolder.create( layoutInflater = layoutInflater, parent = parent, @@ -275,6 +287,17 @@ class MessageListAdapter internal constructor( starClickListener = starClickListener, ) + private fun createComposableMessageViewHolder(parent: ViewGroup): MessageListViewHolder = + ComposableMessageViewHolder.create( + context = parent.context, + themeProvider = themeProvider, + onClick = { listItemListener.onMessageClicked(it) }, + onLongClick = { listItemListener.onToggleMessageSelection(it) }, + onFavouriteClick = { listItemListener.onToggleMessageFlag(it) }, + onAvatarClick = { listItemListener.onToggleMessageSelection(it) }, + appearance = appearance, + ) + override fun onBindViewHolder(holder: MessageListViewHolder, position: Int) { when (val viewType = getItemViewType(position)) { TYPE_IN_APP_NOTIFICATION_BANNER_INLINE_LIST if isInAppNotificationEnabled -> @@ -282,12 +305,22 @@ class MessageListAdapter internal constructor( TYPE_MESSAGE -> { val messageListItem = getItem(position) - val messageViewHolder = holder as MessageViewHolder - messageViewHolder.bind( - messageListItem = messageListItem, - isActive = isActiveMessage(messageListItem), - isSelected = isSelected(messageListItem), - ) + val result = featureFlagProvider.provide(UseComposeForMessageListItems) + if (result.isEnabled()) { + val messageViewHolder = holder as ComposableMessageViewHolder + messageViewHolder.bind( + item = messageListItem, + isActive = isActiveMessage(messageListItem), + isSelected = isSelected(messageListItem), + ) + } else { + val messageViewHolder = holder as MessageViewHolder + messageViewHolder.bind( + messageListItem = messageListItem, + isActive = isActiveMessage(messageListItem), + isSelected = isSelected(messageListItem), + ) + } } TYPE_FOOTER -> { @@ -368,8 +401,13 @@ class MessageListAdapter internal constructor( } private fun getItemFromView(view: View): MessageListItem? { - val messageViewHolder = view.tag as MessageViewHolder - return getItemById(messageViewHolder.uniqueId) + if (featureFlagProvider.provide(UseComposeForMessageListItems).isEnabled()) { + val messageViewHolder = view.tag as ComposableMessageViewHolder + return getItemById(messageViewHolder.uniqueId) + } else { + val messageViewHolder = view.tag as MessageViewHolder + return getItemById(messageViewHolder.uniqueId) + } } } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAppearance.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAppearance.kt index 9958682d09c..33ade74c48c 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAppearance.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAppearance.kt @@ -11,6 +11,9 @@ data class MessageListAppearance( val showContactPicture: Boolean, val showingThreadedList: Boolean, val backGroundAsReadIndicator: Boolean, - val showAccountChip: Boolean, + /** + * Whether to show an account color indicator on the left side of the message item. + */ + val showAccountIndicator: Boolean, val density: UiDensity, ) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFeatureFlags.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFeatureFlags.kt new file mode 100644 index 00000000000..c66c5fc9165 --- /dev/null +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFeatureFlags.kt @@ -0,0 +1,8 @@ +package com.fsck.k9.ui.messagelist + +import net.thunderbird.core.featureflag.FeatureFlagKey + +object MessageListFeatureFlags { + + val UseComposeForMessageListItems = FeatureFlagKey("use_compose_for_message_list_items") +} 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 index ebee1c575c9..5a89a91c8c4 100644 --- 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 @@ -232,7 +232,7 @@ class MessageListFragment : maybeHideFloatingActionButton() } - val isShowAccountChip: Boolean + val isShowAccountIndicator: Boolean get() = isUnifiedFolders || !isSingleAccountMode override fun onAttach(context: Context) { @@ -344,6 +344,7 @@ class MessageListFragment : listItemListener = this, appearance = messageListAppearance, relativeDateTimeFormatter = RelativeDateTimeFormatter(requireContext(), clock), + themeProvider = featureThemeProvider, featureFlagProvider = featureFlagProvider, ).apply { activeMessage = this@MessageListFragment.activeMessage @@ -784,7 +785,7 @@ class MessageListFragment : showingThreadedList = showingThreadedList, backGroundAsReadIndicator = generalSettingsManager .getConfig().display.visualSettings.isUseBackgroundAsUnreadIndicator, - showAccountChip = isShowAccountChip, + showAccountIndicator = isShowAccountIndicator, density = K9.messageListDensity, ) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt index cb388d16930..4a3f14eae32 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt @@ -15,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import app.k9mail.ui.utils.itemtouchhelper.ItemTouchHelper import com.fsck.k9.ui.R +import com.fsck.k9.ui.messagelist.item.ComposableMessageViewHolder import com.fsck.k9.ui.messagelist.item.MessageViewHolder import com.google.android.material.color.ColorRoles import com.google.android.material.textview.MaterialTextView @@ -81,9 +82,7 @@ class MessageListSwipeCallback( } override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: ViewHolder): Int { - if (viewHolder !is MessageViewHolder) return 0 - - val item = adapter.getItemById(viewHolder.uniqueId) ?: return 0 + val item = viewHolder.messageListItem ?: return 0 val (swipeLeftAction, swipeRightAction) = item.swipeActions var swipeFlags = 0 @@ -178,8 +177,7 @@ class MessageListSwipeCallback( if (dX != 0F) { canvas.withTranslation(x = view.left.toFloat(), y = view.top.toFloat()) { - val holder = viewHolder as MessageViewHolder - val item = adapter.getItemById(holder.uniqueId) ?: return@withTranslation + val item = viewHolder.messageListItem ?: return@withTranslation if (isCurrentlyActive || !success) { drawLayout(dX, viewWidth, viewHeight, item) } else { @@ -377,7 +375,11 @@ class MessageListSwipeCallback( } private val ViewHolder.messageListItem: MessageListItem? - get() = (this as? MessageViewHolder)?.uniqueId?.let { adapter.getItemById(it) } + get() = when (this) { + is MessageViewHolder -> adapter.getItemById(uniqueId) + is ComposableMessageViewHolder -> adapter.getItemById(uniqueId) + else -> null + } } fun interface SwipeActionSupportProvider { diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/ComposableMessageViewHolder.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/ComposableMessageViewHolder.kt new file mode 100644 index 00000000000..13d92b69dfb --- /dev/null +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/ComposableMessageViewHolder.kt @@ -0,0 +1,71 @@ +package com.fsck.k9.ui.messagelist.item + +import android.content.Context +import androidx.compose.ui.platform.ComposeView +import com.fsck.k9.ui.messagelist.MessageListAppearance +import com.fsck.k9.ui.messagelist.MessageListItem +import net.thunderbird.core.ui.theme.api.FeatureThemeProvider + +/** + * A composable view holder for message list items. + */ +class ComposableMessageViewHolder( + private val composeView: ComposeView, + private val themeProvider: FeatureThemeProvider, + private val onClick: (MessageListItem) -> Unit, + private val onLongClick: (MessageListItem) -> Unit, + private val onAvatarClick: (MessageListItem) -> Unit, + private val onFavouriteClick: (MessageListItem) -> Unit, + private val appearance: MessageListAppearance, +) : MessageListViewHolder(composeView) { + + var uniqueId: Long = -1L + + fun bind(item: MessageListItem, isActive: Boolean, isSelected: Boolean) { + uniqueId = item.uniqueId + + composeView.setContent { + themeProvider.WithTheme { + MessageItemContent( + item = item, + isActive = isActive, + isSelected = isSelected, + onClick = { onClick(item) }, + onLongClick = { onLongClick(item) }, + onAvatarClick = { onAvatarClick(item) }, + onFavouriteClick = { onFavouriteClick(item) }, + appearance = appearance, + ) + } + } + } + + companion object { + + fun create( + context: Context, + themeProvider: FeatureThemeProvider, + onClick: (MessageListItem) -> Unit, + onLongClick: (MessageListItem) -> Unit, + onFavouriteClick: (MessageListItem) -> Unit, + onAvatarClick: (MessageListItem) -> Unit, + appearance: MessageListAppearance, + ): ComposableMessageViewHolder { + val composeView = ComposeView(context) + + val holder = ComposableMessageViewHolder( + composeView = composeView, + themeProvider = themeProvider, + onClick = onClick, + onLongClick = onLongClick, + onAvatarClick = onAvatarClick, + onFavouriteClick = onFavouriteClick, + appearance = appearance, + ) + + composeView.tag = holder + + return holder + } + } +} diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageItemContent.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageItemContent.kt new file mode 100644 index 00000000000..3ff073a960b --- /dev/null +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageItemContent.kt @@ -0,0 +1,86 @@ +package com.fsck.k9.ui.messagelist.item + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.fsck.k9.ui.messagelist.MessageListAppearance +import com.fsck.k9.ui.messagelist.MessageListItem +import kotlin.time.ExperimentalTime +import kotlin.time.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import net.thunderbird.core.ui.compose.designsystem.organism.message.ActiveMessageItem +import net.thunderbird.core.ui.compose.designsystem.organism.message.ReadMessageItem +import net.thunderbird.core.ui.compose.designsystem.organism.message.UnreadMessageItem + +@Suppress("LongParameterList") +@OptIn(ExperimentalTime::class) +@Composable +internal fun MessageItemContent( + item: MessageListItem, + isActive: Boolean, + isSelected: Boolean, + onClick: () -> Unit, + onLongClick: () -> Unit, + onAvatarClick: () -> Unit, + onFavouriteClick: (Boolean) -> Unit, + appearance: MessageListAppearance, +) { + val receivedAt = remember(item.messageDate) { + Instant.fromEpochMilliseconds(item.messageDate) + .toLocalDateTime(TimeZone.currentSystemDefault()) + } + + when { + isActive -> ActiveMessageItem( + sender = "${item.displayName}", + subject = item.subject ?: "n/a", + preview = item.previewText, + receivedAt = receivedAt, + avatar = {}, + onClick = onClick, + onLongClick = onLongClick, + onLeadingClick = onAvatarClick, + onFavouriteChange = onFavouriteClick, + favourite = item.isStarred, + selected = isSelected, + maxPreviewLines = appearance.previewLines, + threadCount = item.threadCount, + hasAttachments = item.hasAttachments, + swapSenderWithSubject = !appearance.senderAboveSubject, + ) + item.isRead -> ReadMessageItem( + sender = "${item.displayName}", + subject = item.subject ?: "n/a", + preview = item.previewText, + receivedAt = receivedAt, + avatar = {}, + onClick = onClick, + onLongClick = onLongClick, + onLeadingClick = onAvatarClick, + onFavouriteChange = onFavouriteClick, + favourite = item.isStarred, + selected = isSelected, + maxPreviewLines = appearance.previewLines, + threadCount = item.threadCount, + hasAttachments = item.hasAttachments, + swapSenderWithSubject = !appearance.senderAboveSubject, + ) + else -> UnreadMessageItem( + sender = "${item.displayName}", + subject = item.subject ?: "n/a", + preview = item.previewText, + receivedAt = receivedAt, + avatar = {}, + onClick = onClick, + onLongClick = onLongClick, + onLeadingClick = onAvatarClick, + onFavouriteChange = onFavouriteClick, + favourite = item.isStarred, + selected = isSelected, + maxPreviewLines = appearance.previewLines, + threadCount = item.threadCount, + hasAttachments = item.hasAttachments, + swapSenderWithSubject = !appearance.senderAboveSubject, + ) + } +} diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageViewHolder.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageViewHolder.kt index 1ab2c3a517b..7398575f283 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageViewHolder.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageViewHolder.kt @@ -89,10 +89,10 @@ class MessageViewHolder( threadCount = displayThreadCount, ) - if (appearance.showAccountChip) { - val accountChipDrawable = chipView.drawable.mutate() - DrawableCompat.setTint(accountChipDrawable, account.profile.color) - chipView.setImageDrawable(accountChipDrawable) + if (appearance.showAccountIndicator) { + val accountIndicatorDrawable = chipView.drawable.mutate() + DrawableCompat.setTint(accountIndicatorDrawable, account.profile.color) + chipView.setImageDrawable(accountIndicatorDrawable) } if (appearance.stars) { @@ -315,7 +315,7 @@ class MessageViewHolder( holder.contactPictureView.isVisible = false } - holder.chipView.isVisible = appearance.showAccountChip + holder.chipView.isVisible = appearance.showAccountIndicator // 1 preview line is needed even if it is set to 0, because subject is part of the same text view holder.previewView.maxLines = max(appearance.previewLines, 1) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageTopView.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageTopView.kt index 18b434ee127..3ab27108564 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageTopView.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageTopView.kt @@ -57,7 +57,7 @@ class MessageTopView( private var isShowingProgress = false private var showPicturesButtonClicked = false - private var showAccountChip = false + private var showAccountIndicator = false private var messageCryptoPresenter: MessageCryptoPresenter? = null @@ -83,8 +83,8 @@ class MessageTopView( hideHeaderView() } - fun setShowAccountChip(showAccountChip: Boolean) { - this.showAccountChip = showAccountChip + fun setShowAccountIndicator(showAccountIndicator: Boolean) { + this.showAccountIndicator = showAccountIndicator } private fun setShowPicturesButtonListener() { @@ -197,7 +197,7 @@ class MessageTopView( } fun setHeaders(message: Message?, account: LegacyAccountDto?, showStar: Boolean) { - messageHeaderView.populate(message, account, showStar, showAccountChip) + messageHeaderView.populate(message, account, showStar, showAccountIndicator) messageHeaderView.visibility = VISIBLE } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt index 0673ad6de7b..d5fec98afbc 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewContainerFragment.kt @@ -30,7 +30,7 @@ class MessageViewContainerFragment : Fragment() { setMenuVisibility(value) } - private var showAccountChip: Boolean = true + private var showAccountIndicator: Boolean = true lateinit var messageReference: MessageReference private set @@ -68,9 +68,9 @@ class MessageViewContainerFragment : Fragment() { ?: error("Missing state $STATE_MESSAGE_REFERENCE") } - showAccountChip = arguments?.getBoolean(ARG_SHOW_ACCOUNT_CHIP) ?: showAccountChip + showAccountIndicator = arguments?.getBoolean(ARG_SHOW_ACCOUNT_INDICATOR) ?: showAccountIndicator - adapter = MessageViewContainerAdapter(this, showAccountChip) + adapter = MessageViewContainerAdapter(this, showAccountIndicator) } override fun onAttach(context: Context) { @@ -230,7 +230,7 @@ class MessageViewContainerFragment : Fragment() { private class MessageViewContainerAdapter( fragment: Fragment, - private val showAccountChip: Boolean, + private val showAccountIndicator: Boolean, ) : FragmentStateAdapter(fragment) { var messageList: List = emptyList() @@ -260,7 +260,7 @@ class MessageViewContainerFragment : Fragment() { check(position in messageList.indices) val messageReference = messageList[position].messageReference - return MessageViewFragment.newInstance(messageReference, showAccountChip) + return MessageViewFragment.newInstance(messageReference, showAccountIndicator) } fun getMessageReference(position: Int): MessageReference? { @@ -304,17 +304,17 @@ class MessageViewContainerFragment : Fragment() { private const val VIEW_PAGER_SWIPE_VELOCITY_THRESHOLD = 0.8f private const val ARG_REFERENCE = "reference" - private const val ARG_SHOW_ACCOUNT_CHIP = "showAccountChip" + private const val ARG_SHOW_ACCOUNT_INDICATOR = "showAccountIndicator" private const val STATE_MESSAGE_REFERENCE = "messageReference" fun newInstance( reference: MessageReference, - showAccountChip: Boolean, + isShowAccountIndicator: Boolean, ): MessageViewContainerFragment { return MessageViewContainerFragment().withArguments( ARG_REFERENCE to reference.toIdentityString(), - ARG_SHOW_ACCOUNT_CHIP to showAccountChip, + ARG_SHOW_ACCOUNT_INDICATOR to isShowAccountIndicator, ) } } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index dd037df751b..66072c2e281 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -107,7 +107,7 @@ class MessageViewFragment : private lateinit var account: LegacyAccountDto lateinit var messageReference: MessageReference - private var showAccountChip: Boolean = true + private var showAccountIndicator: Boolean = true private var currentAttachmentViewInfo: AttachmentViewInfo? = null private var isDeleteMenuItemDisabled: Boolean = false @@ -140,8 +140,8 @@ class MessageViewFragment : messageReference = MessageReference.parse(arguments?.getString(ARG_REFERENCE)) ?: error("Invalid argument '$ARG_REFERENCE'") - showAccountChip = arguments?.getBoolean(ARG_SHOW_ACCOUNT_CHIP) - ?: error("Missing argument: '$ARG_SHOW_ACCOUNT_CHIP'") + showAccountIndicator = arguments?.getBoolean(ARG_SHOW_ACCOUNT_INDICATOR) + ?: error("Missing argument: '$ARG_SHOW_ACCOUNT_INDICATOR'") if (savedInstanceState != null) { wasMessageMarkedAsOpened = savedInstanceState.getBoolean(STATE_WAS_MESSAGE_MARKED_AS_OPENED) @@ -173,7 +173,7 @@ class MessageViewFragment : } private fun initializeMessageTopView(messageTopView: MessageTopView) { - messageTopView.setShowAccountChip(showAccountChip) + messageTopView.setShowAccountIndicator(showAccountIndicator) messageTopView.setAttachmentCallback(this) messageTopView.setMessageCryptoPresenter(messageCryptoPresenter) @@ -995,15 +995,15 @@ class MessageViewFragment : const val PROGRESS_THRESHOLD_MILLIS = 500 * 1000 private const val ARG_REFERENCE = "reference" - private const val ARG_SHOW_ACCOUNT_CHIP = "showAccountChip" + private const val ARG_SHOW_ACCOUNT_INDICATOR = "showAccountIndicator" private const val STATE_WAS_MESSAGE_MARKED_AS_OPENED = "wasMessageMarkedAsOpened" private const val STATE_IS_ACTIVE = "isActive" - fun newInstance(reference: MessageReference, showAccountChip: Boolean): MessageViewFragment { + fun newInstance(reference: MessageReference, showAccountIndicator: Boolean): MessageViewFragment { return MessageViewFragment().withArguments( ARG_REFERENCE to reference.toIdentityString(), - ARG_SHOW_ACCOUNT_CHIP to showAccountChip, + ARG_SHOW_ACCOUNT_INDICATOR to showAccountIndicator, ) } } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/view/MessageHeader.java b/legacy/ui/legacy/src/main/java/com/fsck/k9/view/MessageHeader.java index c2330fee9dc..666e0e6915f 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/view/MessageHeader.java +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/view/MessageHeader.java @@ -195,8 +195,8 @@ public void setOnFlagListener(OnClickListener listener) { starView.setOnClickListener(listener); } - public void populate(final Message message, final LegacyAccountDto account, boolean showStar, boolean showAccountChip) { - if (showAccountChip) { + public void populate(final Message message, final LegacyAccountDto account, boolean showStar, boolean showAccountIndicator) { + if (showAccountIndicator) { accountNameView.setVisibility(View.VISIBLE); accountNameView.setText(account.getDisplayName()); accountNameView.setChipBackgroundColor(ColorStateList.valueOf(account.getChipColor())); diff --git a/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt b/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt index ba843fdcfcb..97da1876546 100644 --- a/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt +++ b/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt @@ -8,6 +8,7 @@ import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.Composable import androidx.core.view.isGone import androidx.core.view.isVisible import assertk.Assert @@ -31,8 +32,11 @@ import kotlin.time.ExperimentalTime import net.thunderbird.core.android.account.Identity import net.thunderbird.core.android.account.LegacyAccount import net.thunderbird.core.android.testing.RobolectricTest +import net.thunderbird.core.featureflag.FeatureFlagKey +import net.thunderbird.core.featureflag.FeatureFlagProvider import net.thunderbird.core.featureflag.FeatureFlagResult import net.thunderbird.core.testing.TestClock +import net.thunderbird.core.ui.theme.api.FeatureThemeProvider import net.thunderbird.feature.account.AccountIdFactory import net.thunderbird.feature.account.storage.profile.AvatarDto import net.thunderbird.feature.account.storage.profile.AvatarTypeDto @@ -54,21 +58,21 @@ class MessageListAdapterTest : RobolectricTest() { val listItemListener: MessageListItemActionListener = mock() @Test - fun withShowAccountChip_shouldShowAccountChip() { - val adapter = createAdapter(showAccountChip = true) + fun withShowAccountIndicator_shouldShowAccountIndicator() { + val adapter = createAdapter(showAccountIndicator = true) val view = adapter.createAndBindView() - assertThat(view.accountChipView).isVisible() + assertThat(view.accountIndicatorView).isVisible() } @Test - fun withoutShowAccountChip_shouldHideAccountChip() { - val adapter = createAdapter(showAccountChip = false) + fun withoutShowAccountIndicator_shouldHideAccountIndicator() { + val adapter = createAdapter(showAccountIndicator = false) val view = adapter.createAndBindView() - assertThat(view.accountChipView).isGone() + assertThat(view.accountIndicatorView).isGone() } @Test @@ -408,7 +412,7 @@ class MessageListAdapterTest : RobolectricTest() { showContactPicture: Boolean = true, showingThreadedList: Boolean = true, backGroundAsReadIndicator: Boolean = false, - showAccountChip: Boolean = false, + showAccountIndicator: Boolean = false, density: UiDensity = UiDensity.Default, ): MessageListAdapter { val appearance = MessageListAppearance( @@ -419,7 +423,7 @@ class MessageListAdapterTest : RobolectricTest() { showContactPicture, showingThreadedList, backGroundAsReadIndicator, - showAccountChip, + showAccountIndicator, density, ) @@ -432,7 +436,8 @@ class MessageListAdapterTest : RobolectricTest() { listItemListener = listItemListener, appearance = appearance, relativeDateTimeFormatter = RelativeDateTimeFormatter(context, TestClock()), - featureFlagProvider = { FeatureFlagResult.Disabled }, + themeProvider = FakeThemeProvider(), + featureFlagProvider = FakeFeatureFlagProvider(), ) } @@ -534,7 +539,7 @@ class MessageListAdapterTest : RobolectricTest() { fun secondLine(senderOrSubject: String, preview: String) = "$senderOrSubject – $preview" - val View.accountChipView: View get() = findViewById(R.id.account_color_chip) + val View.accountIndicatorView: View get() = findViewById(R.id.account_color_chip) val View.starView: View get() = findViewById(R.id.star) val View.contactPictureContainerView: View get() = findViewById(R.id.contact_picture_click_area) val View.threadCountView: MaterialTextView get() = findViewById(R.id.thread_count) @@ -578,4 +583,24 @@ class MessageListAdapterTest : RobolectricTest() { private val MaterialTextView.textString: String get() = text.toString() + + private class FakeThemeProvider : FeatureThemeProvider { + @Composable + override fun WithTheme(content: @Composable (() -> Unit)) { + content() + } + + @Composable + override fun WithTheme( + darkTheme: Boolean, + content: @Composable (() -> Unit), + ) { + content() + } + } + + private class FakeFeatureFlagProvider : FeatureFlagProvider { + // Disabled as the test is primarily concerned with the XML based UI + override fun provide(key: FeatureFlagKey): FeatureFlagResult = FeatureFlagResult.Disabled + } }