Loading legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt +30 −351 Original line number Diff line number Diff line Loading @@ -3,40 +3,21 @@ package com.fsck.k9.ui.messagelist import android.annotation.SuppressLint import android.content.res.Resources import android.content.res.Resources.Theme import android.graphics.Typeface import android.graphics.drawable.Drawable import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.AbsoluteSizeSpan import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan import android.view.LayoutInflater import android.view.View import android.view.View.OnClickListener import android.view.View.OnLongClickListener import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.DimenRes import androidx.constraintlayout.widget.Guideline import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_POSITION import app.k9mail.core.ui.legacy.designsystem.atom.icon.Icons import app.k9mail.legacy.message.controller.MessageReference import com.fsck.k9.FontSizes import com.fsck.k9.UiDensity import com.fsck.k9.contacts.ContactPictureLoader import com.fsck.k9.mail.Address import com.fsck.k9.ui.R import com.fsck.k9.ui.helper.RelativeDateTimeFormatter import com.fsck.k9.ui.resolveColorAttribute import com.google.android.material.textview.MaterialTextView import kotlin.math.max import com.google.android.material.R as MaterialR import com.fsck.k9.ui.messagelist.item.FooterViewHolder import com.fsck.k9.ui.messagelist.item.MessageListViewHolder import com.fsck.k9.ui.messagelist.item.MessageViewHolder import com.fsck.k9.ui.messagelist.item.MessageViewHolderColors private const val FOOTER_ID = 1L Loading @@ -44,7 +25,7 @@ private const val TYPE_MESSAGE = 0 private const val TYPE_FOOTER = 1 class MessageListAdapter internal constructor( theme: Theme, private val theme: Theme, private val res: Resources, private val layoutInflater: LayoutInflater, private val contactsPictureLoader: ContactPictureLoader, Loading @@ -53,39 +34,7 @@ class MessageListAdapter internal constructor( private val relativeDateTimeFormatter: RelativeDateTimeFormatter, ) : RecyclerView.Adapter<MessageListViewHolder>() { private val forwardedIcon: Drawable = ResourcesCompat.getDrawable(res, Icons.Outlined.Forward, theme)!! private val answeredIcon: Drawable = ResourcesCompat.getDrawable(res, Icons.Outlined.Reply, theme)!! private val forwardedAnsweredIcon: Drawable = ResourcesCompat.getDrawable(res, Icons.Outlined.CompareArrows, theme)!! private val activeItemBackgroundColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorSecondaryContainer) private val selectedItemBackgroundColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorSurfaceVariant) private val regularItemBackgroundColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorSurface) private val readItemBackgroundColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorSurfaceContainerHigh) private val unreadItemBackgroundColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorSurface) private val activeItemColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOnSecondaryContainer) private val selectedItemColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOnSurfaceVariant) private val regularItemColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOnSurface) private val readItemColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOutline) private val unreadItemColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOnSurface) private val previewTextColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOutline) private val previewActiveTextColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOnSecondary) private val compactVerticalPadding = res.getDimensionPixelSize(R.dimen.messageListCompactVerticalPadding) private val compactTextViewMarginTop = res.getDimensionPixelSize(R.dimen.messageListCompactTextViewMargin) private val compactLineSpacingMultiplier = res.getFloatCompat(R.dimen.messageListCompactLineSpacingMultiplier) private val defaultVerticalPadding = res.getDimensionPixelSize(R.dimen.messageListDefaultVerticalPadding) private val defaultTextViewMarginTop = res.getDimensionPixelSize(R.dimen.messageListDefaultTextViewMargin) private val defaultLineSpacingMultiplier = res.getFloatCompat(R.dimen.messageListDefaultLineSpacingMultiplier) private val relaxedVerticalPadding = res.getDimensionPixelSize(R.dimen.messageListRelaxedVerticalPadding) private val relaxedTextViewMarginTop = res.getDimensionPixelSize(R.dimen.messageListRelaxedTextViewMargin) private val relaxedLineSpacingMultiplier = res.getFloatCompat(R.dimen.messageListRelaxedLineSpacingMultiplier) val colors: MessageViewHolderColors = MessageViewHolderColors.resolveColors(theme) var messages: List<MessageListItem> = emptyList() @SuppressLint("NotifyDataSetChanged") Loading Loading @@ -184,13 +133,6 @@ class MessageListAdapter internal constructor( private val footerPosition: Int get() = if (hasFooter) lastMessagePosition + 1 else NO_POSITION private inline val subjectViewFontSize: Int get() = if (appearance.senderAboveSubject) { appearance.fontSizes.messageListSender } else { appearance.fontSizes.messageListSubject } private val messageClickedListener = OnClickListener { view: View -> val messageListItem = getItemFromView(view) ?: return@OnClickListener listItemListener.onMessageClicked(messageListItem) Loading Loading @@ -266,98 +208,42 @@ class MessageListAdapter internal constructor( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageListViewHolder { return when (viewType) { TYPE_MESSAGE -> createMessageViewHolder(parent) TYPE_FOOTER -> createFooterViewHolder(parent) TYPE_FOOTER -> FooterViewHolder.create(layoutInflater, parent, footerClickListener) else -> error("Unsupported type: $viewType") } } private fun createMessageViewHolder(parent: ViewGroup?): MessageViewHolder { val view = layoutInflater.inflate(R.layout.message_list_item, parent, false) view.setOnClickListener(messageClickedListener) view.setOnLongClickListener(messageLongClickedListener) val holder = MessageViewHolder(view) val contactPictureClickArea = view.findViewById<View>(R.id.contact_picture_click_area) if (appearance.showContactPicture) { contactPictureClickArea.setOnClickListener(contactPictureContainerClickListener) } else { contactPictureClickArea.isVisible = false holder.selected.isVisible = false holder.contactPicture.isVisible = false } holder.chip.isVisible = appearance.showAccountChip appearance.fontSizes.setViewTextSize(holder.subject, subjectViewFontSize) appearance.fontSizes.setViewTextSize(holder.date, appearance.fontSizes.messageListDate) // 1 preview line is needed even if it is set to 0, because subject is part of the same text view holder.preview.maxLines = max(appearance.previewLines, 1) appearance.fontSizes.setViewTextSize(holder.preview, appearance.fontSizes.messageListPreview) appearance.fontSizes.setViewTextSize( holder.threadCount, appearance.fontSizes.messageListSubject, ) // thread count is next to subject holder.star.isVisible = appearance.stars holder.starClickArea.isVisible = appearance.stars holder.starClickArea.setOnClickListener(starClickListener) applyDensityValue(holder, appearance.density) view.tag = holder return holder } private fun applyDensityValue(holder: MessageViewHolder, density: UiDensity) { val verticalPadding: Int val textViewMarginTop: Int val lineSpacingMultiplier: Float when (density) { UiDensity.Compact -> { verticalPadding = compactVerticalPadding textViewMarginTop = compactTextViewMarginTop lineSpacingMultiplier = compactLineSpacingMultiplier } UiDensity.Default -> { verticalPadding = defaultVerticalPadding textViewMarginTop = defaultTextViewMarginTop lineSpacingMultiplier = defaultLineSpacingMultiplier } UiDensity.Relaxed -> { verticalPadding = relaxedVerticalPadding textViewMarginTop = relaxedTextViewMarginTop lineSpacingMultiplier = relaxedLineSpacingMultiplier } } holder.itemView.findViewById<Guideline>(R.id.top_guideline).setGuidelineBegin(verticalPadding) holder.itemView.findViewById<Guideline>(R.id.bottom_guideline).setGuidelineEnd(verticalPadding) holder.preview.apply { setMarginTop(textViewMarginTop) setLineSpacing(lineSpacingExtra, lineSpacingMultiplier) } } private fun createFooterViewHolder(parent: ViewGroup): MessageListViewHolder { val view = layoutInflater.inflate(R.layout.message_list_item_footer, parent, false) view.setOnClickListener(footerClickListener) return FooterViewHolder(view) } private fun createMessageViewHolder(parent: ViewGroup?): MessageViewHolder = MessageViewHolder.create( layoutInflater = layoutInflater, parent = parent, appearance = appearance, res = res, contactsPictureLoader = contactsPictureLoader, relativeDateTimeFormatter = relativeDateTimeFormatter, colors = colors, theme = theme, onClickListener = messageClickedListener, onLongClickListener = messageLongClickedListener, contactPictureContainerClickListener = contactPictureContainerClickListener, starClickListener = starClickListener, ) override fun onBindViewHolder(holder: MessageListViewHolder, position: Int) { when (val viewType = getItemViewType(position)) { TYPE_MESSAGE -> { val messageListItem = getItem(position) bindMessageViewHolder(holder as MessageViewHolder, messageListItem) val messageViewHolder = holder as MessageViewHolder messageViewHolder.bind( messageListItem = messageListItem, isActive = isActiveMessage(messageListItem), isSelected = isSelected(messageListItem), ) } TYPE_FOOTER -> { bindFooterViewHolder(holder as FooterViewHolder) val footerViewHolder = holder as FooterViewHolder footerViewHolder.bind(footerText) } else -> { Loading @@ -366,207 +252,6 @@ class MessageListAdapter internal constructor( } } private fun bindMessageViewHolder(holder: MessageViewHolder, messageListItem: MessageListItem) { val isSelected = selected.contains(messageListItem.uniqueId) val isActive = isActiveMessage(messageListItem) if (appearance.showContactPicture) { holder.contactPictureClickArea.isSelected = isSelected if (isSelected) { holder.contactPicture.isVisible = false holder.selected.isVisible = true } else { holder.selected.isVisible = false holder.contactPicture.isVisible = true } holder.contactPictureClickArea.contentDescription = if (isSelected) { res.getString(R.string.swipe_action_deselect) } else { res.getString(R.string.swipe_action_select) } } with(messageListItem) { val foregroundColor = selectForegroundColor(isSelected, isRead, isActive) val maybeBoldTypeface = if (isRead) Typeface.NORMAL else Typeface.BOLD val displayDate = relativeDateTimeFormatter.formatDate(messageDate) val displayThreadCount = if (appearance.showingThreadedList) threadCount else 0 val subject = MlfUtils.buildSubject(subject, res.getString(R.string.general_no_subject), displayThreadCount) if (appearance.showAccountChip) { val accountChipDrawable = holder.chip.drawable.mutate() DrawableCompat.setTint(accountChipDrawable, account.chipColor) holder.chip.setImageDrawable(accountChipDrawable) } if (appearance.stars) { holder.star.isSelected = isStarred if (isStarred) { holder.star.clearColorFilter() } else { holder.star.setColorFilter(foregroundColor) } holder.starClickArea.contentDescription = if (isStarred) { res.getString(R.string.unflag_action) } else { res.getString(R.string.flag_action) } } holder.uniqueId = uniqueId if (appearance.showContactPicture && holder.contactPicture.isVisible) { setContactPicture(holder.contactPicture, displayAddress) } holder.itemView.setBackgroundColor(selectBackgroundColor(isSelected, isRead, isActive)) updateWithThreadCount(holder, displayThreadCount) val beforePreviewText = if (appearance.senderAboveSubject) subject else displayName val messageStringBuilder = SpannableStringBuilder(beforePreviewText) if (appearance.previewLines > 0) { val preview = getPreview(isMessageEncrypted, previewText) if (preview.isNotEmpty()) { messageStringBuilder.append(" – ").append(preview) } } holder.preview.setTextColor(foregroundColor) holder.preview.setText(messageStringBuilder, TextView.BufferType.SPANNABLE) formatPreviewText(holder.preview, beforePreviewText, isRead, isActive, isSelected) holder.subject.typeface = Typeface.create(holder.subject.typeface, maybeBoldTypeface) holder.subject.setTextColor(foregroundColor) val firstLineText = if (appearance.senderAboveSubject) displayName else subject holder.subject.text = firstLineText holder.subject.contentDescription = if (isRead) { null } else { res.getString(R.string.message_list_content_description_unread_prefix, firstLineText) } holder.date.typeface = Typeface.create(holder.date.typeface, maybeBoldTypeface) holder.date.setTextColor(foregroundColor) holder.date.text = displayDate holder.attachment.isVisible = hasAttachments holder.attachment.setColorFilter(foregroundColor) val statusHolder = buildStatusHolder(isForwarded, isAnswered) if (statusHolder != null) { holder.status.setImageDrawable(statusHolder) holder.status.isVisible = true } else { holder.status.isVisible = false } } } private fun bindFooterViewHolder(holder: FooterViewHolder) { holder.text.text = footerText } private fun formatPreviewText( preview: MaterialTextView, beforePreviewText: CharSequence, messageRead: Boolean, active: Boolean, selected: Boolean, ) { val previewText = preview.text as Spannable val textColor = selectPreviewTextColor(active, selected) val beforePreviewLength = beforePreviewText.length addBeforePreviewSpan(previewText, beforePreviewLength, messageRead) previewText.setSpan( ForegroundColorSpan(textColor), beforePreviewLength, previewText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, ) } private fun addBeforePreviewSpan(text: Spannable, length: Int, messageRead: Boolean) { val fontSize = if (appearance.senderAboveSubject) { appearance.fontSizes.messageListSubject } else { appearance.fontSizes.messageListSender } if (fontSize != FontSizes.FONT_DEFAULT) { val span = AbsoluteSizeSpan(fontSize, true) text.setSpan(span, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } if (!messageRead) { val span = StyleSpan(Typeface.BOLD) text.setSpan(span, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } } private fun setContactPicture(contactPictureView: ImageView, displayAddress: Address?) { if (displayAddress != null) { contactsPictureLoader.setContactPicture(contactPictureView, displayAddress) } else { contactPictureView.setImageResource(Icons.Outlined.Check) } } private fun buildStatusHolder(forwarded: Boolean, answered: Boolean): Drawable? { if (forwarded && answered) { return forwardedAnsweredIcon } else if (answered) { return answeredIcon } else if (forwarded) { return forwardedIcon } return null } private fun selectBackgroundColor(selected: Boolean, read: Boolean, active: Boolean): Int { val backGroundAsReadIndicator = appearance.backGroundAsReadIndicator return when { selected -> selectedItemBackgroundColor active -> activeItemBackgroundColor backGroundAsReadIndicator && read -> readItemBackgroundColor backGroundAsReadIndicator && !read -> unreadItemBackgroundColor else -> regularItemBackgroundColor } } private fun selectForegroundColor(selected: Boolean, read: Boolean, active: Boolean): Int { return when { selected -> selectedItemColor active -> activeItemColor read -> readItemColor !read -> unreadItemColor else -> regularItemColor } } private fun selectPreviewTextColor(active: Boolean, selected: Boolean): Int { return when { selected -> previewTextColor active -> previewActiveTextColor else -> previewTextColor } } private fun updateWithThreadCount(holder: MessageViewHolder, threadCount: Int) { if (threadCount > 1) { holder.threadCount.text = String.format("%d", threadCount) holder.threadCount.isVisible = true } else { holder.threadCount.isVisible = false } } private fun getPreview(isMessageEncrypted: Boolean, previewText: String): String { return if (isMessageEncrypted) { res.getString(R.string.preview_encrypted) } else { previewText } } private fun isActiveMessage(item: MessageListItem): Boolean { val activeMessage = this.activeMessage ?: return false Loading Loading @@ -639,12 +324,6 @@ class MessageListAdapter internal constructor( } } private fun Resources.getFloatCompat(@DimenRes resId: Int) = ResourcesCompat.getFloat(this, resId) private fun View.setMarginTop(margin: Int) { (layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin = margin } private class MessageListDiffCallback( private val oldMessageList: List<MessageListItem>, private val newMessageList: List<MessageListItem>, Loading legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt +1 −0 Original line number Diff line number Diff line Loading @@ -53,6 +53,7 @@ import com.fsck.k9.ui.choosefolder.ChooseFolderActivity import com.fsck.k9.ui.choosefolder.ChooseFolderResultContract import com.fsck.k9.ui.helper.RelativeDateTimeFormatter import com.fsck.k9.ui.messagelist.MessageListFragment.MessageListFragmentListener.Companion.MAX_PROGRESS import com.fsck.k9.ui.messagelist.item.MessageViewHolder import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback import com.google.android.material.snackbar.Snackbar Loading legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt +1 −0 Original line number Diff line number Diff line Loading @@ -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.MessageViewHolder import com.google.android.material.color.ColorRoles import com.google.android.material.textview.MaterialTextView import kotlin.math.abs Loading legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListViewHolder.ktdeleted 100644 → 0 +0 −30 Original line number Diff line number Diff line package com.fsck.k9.ui.messagelist import android.view.View import android.widget.ImageView import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.fsck.k9.ui.R import com.google.android.material.textview.MaterialTextView sealed class MessageListViewHolder(view: View) : ViewHolder(view) class MessageViewHolder(view: View) : MessageListViewHolder(view) { var uniqueId: Long = -1L val selected: View = view.findViewById(R.id.selected) val contactPicture: ImageView = view.findViewById(R.id.contact_picture) val contactPictureClickArea: View = view.findViewById(R.id.contact_picture_click_area) val subject: MaterialTextView = view.findViewById(R.id.subject) val preview: MaterialTextView = view.findViewById(R.id.preview) val date: MaterialTextView = view.findViewById(R.id.date) val chip: ImageView = view.findViewById(R.id.account_color_chip) val threadCount: MaterialTextView = view.findViewById(R.id.thread_count) val star: ImageView = view.findViewById(R.id.star) val starClickArea: View = view.findViewById(R.id.star_click_area) val attachment: ImageView = view.findViewById(R.id.attachment) val status: ImageView = view.findViewById(R.id.status) } class FooterViewHolder(view: View) : MessageListViewHolder(view) { val text: MaterialTextView = view.findViewById(R.id.main_text) } legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MlfUtils.java +1 −1 Original line number Diff line number Diff line Loading @@ -29,7 +29,7 @@ public class MlfUtils { account.setLastSelectedFolderId(folderId); } static String buildSubject(String subjectFromCursor, String emptySubject, int threadCount) { public static String buildSubject(String subjectFromCursor, String emptySubject, int threadCount) { if (TextUtils.isEmpty(subjectFromCursor)) { return emptySubject; } else if (threadCount > 1) { Loading Loading
legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt +30 −351 Original line number Diff line number Diff line Loading @@ -3,40 +3,21 @@ package com.fsck.k9.ui.messagelist import android.annotation.SuppressLint import android.content.res.Resources import android.content.res.Resources.Theme import android.graphics.Typeface import android.graphics.drawable.Drawable import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.AbsoluteSizeSpan import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan import android.view.LayoutInflater import android.view.View import android.view.View.OnClickListener import android.view.View.OnLongClickListener import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.DimenRes import androidx.constraintlayout.widget.Guideline import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_POSITION import app.k9mail.core.ui.legacy.designsystem.atom.icon.Icons import app.k9mail.legacy.message.controller.MessageReference import com.fsck.k9.FontSizes import com.fsck.k9.UiDensity import com.fsck.k9.contacts.ContactPictureLoader import com.fsck.k9.mail.Address import com.fsck.k9.ui.R import com.fsck.k9.ui.helper.RelativeDateTimeFormatter import com.fsck.k9.ui.resolveColorAttribute import com.google.android.material.textview.MaterialTextView import kotlin.math.max import com.google.android.material.R as MaterialR import com.fsck.k9.ui.messagelist.item.FooterViewHolder import com.fsck.k9.ui.messagelist.item.MessageListViewHolder import com.fsck.k9.ui.messagelist.item.MessageViewHolder import com.fsck.k9.ui.messagelist.item.MessageViewHolderColors private const val FOOTER_ID = 1L Loading @@ -44,7 +25,7 @@ private const val TYPE_MESSAGE = 0 private const val TYPE_FOOTER = 1 class MessageListAdapter internal constructor( theme: Theme, private val theme: Theme, private val res: Resources, private val layoutInflater: LayoutInflater, private val contactsPictureLoader: ContactPictureLoader, Loading @@ -53,39 +34,7 @@ class MessageListAdapter internal constructor( private val relativeDateTimeFormatter: RelativeDateTimeFormatter, ) : RecyclerView.Adapter<MessageListViewHolder>() { private val forwardedIcon: Drawable = ResourcesCompat.getDrawable(res, Icons.Outlined.Forward, theme)!! private val answeredIcon: Drawable = ResourcesCompat.getDrawable(res, Icons.Outlined.Reply, theme)!! private val forwardedAnsweredIcon: Drawable = ResourcesCompat.getDrawable(res, Icons.Outlined.CompareArrows, theme)!! private val activeItemBackgroundColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorSecondaryContainer) private val selectedItemBackgroundColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorSurfaceVariant) private val regularItemBackgroundColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorSurface) private val readItemBackgroundColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorSurfaceContainerHigh) private val unreadItemBackgroundColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorSurface) private val activeItemColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOnSecondaryContainer) private val selectedItemColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOnSurfaceVariant) private val regularItemColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOnSurface) private val readItemColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOutline) private val unreadItemColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOnSurface) private val previewTextColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOutline) private val previewActiveTextColor: Int = theme.resolveColorAttribute(MaterialR.attr.colorOnSecondary) private val compactVerticalPadding = res.getDimensionPixelSize(R.dimen.messageListCompactVerticalPadding) private val compactTextViewMarginTop = res.getDimensionPixelSize(R.dimen.messageListCompactTextViewMargin) private val compactLineSpacingMultiplier = res.getFloatCompat(R.dimen.messageListCompactLineSpacingMultiplier) private val defaultVerticalPadding = res.getDimensionPixelSize(R.dimen.messageListDefaultVerticalPadding) private val defaultTextViewMarginTop = res.getDimensionPixelSize(R.dimen.messageListDefaultTextViewMargin) private val defaultLineSpacingMultiplier = res.getFloatCompat(R.dimen.messageListDefaultLineSpacingMultiplier) private val relaxedVerticalPadding = res.getDimensionPixelSize(R.dimen.messageListRelaxedVerticalPadding) private val relaxedTextViewMarginTop = res.getDimensionPixelSize(R.dimen.messageListRelaxedTextViewMargin) private val relaxedLineSpacingMultiplier = res.getFloatCompat(R.dimen.messageListRelaxedLineSpacingMultiplier) val colors: MessageViewHolderColors = MessageViewHolderColors.resolveColors(theme) var messages: List<MessageListItem> = emptyList() @SuppressLint("NotifyDataSetChanged") Loading Loading @@ -184,13 +133,6 @@ class MessageListAdapter internal constructor( private val footerPosition: Int get() = if (hasFooter) lastMessagePosition + 1 else NO_POSITION private inline val subjectViewFontSize: Int get() = if (appearance.senderAboveSubject) { appearance.fontSizes.messageListSender } else { appearance.fontSizes.messageListSubject } private val messageClickedListener = OnClickListener { view: View -> val messageListItem = getItemFromView(view) ?: return@OnClickListener listItemListener.onMessageClicked(messageListItem) Loading Loading @@ -266,98 +208,42 @@ class MessageListAdapter internal constructor( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageListViewHolder { return when (viewType) { TYPE_MESSAGE -> createMessageViewHolder(parent) TYPE_FOOTER -> createFooterViewHolder(parent) TYPE_FOOTER -> FooterViewHolder.create(layoutInflater, parent, footerClickListener) else -> error("Unsupported type: $viewType") } } private fun createMessageViewHolder(parent: ViewGroup?): MessageViewHolder { val view = layoutInflater.inflate(R.layout.message_list_item, parent, false) view.setOnClickListener(messageClickedListener) view.setOnLongClickListener(messageLongClickedListener) val holder = MessageViewHolder(view) val contactPictureClickArea = view.findViewById<View>(R.id.contact_picture_click_area) if (appearance.showContactPicture) { contactPictureClickArea.setOnClickListener(contactPictureContainerClickListener) } else { contactPictureClickArea.isVisible = false holder.selected.isVisible = false holder.contactPicture.isVisible = false } holder.chip.isVisible = appearance.showAccountChip appearance.fontSizes.setViewTextSize(holder.subject, subjectViewFontSize) appearance.fontSizes.setViewTextSize(holder.date, appearance.fontSizes.messageListDate) // 1 preview line is needed even if it is set to 0, because subject is part of the same text view holder.preview.maxLines = max(appearance.previewLines, 1) appearance.fontSizes.setViewTextSize(holder.preview, appearance.fontSizes.messageListPreview) appearance.fontSizes.setViewTextSize( holder.threadCount, appearance.fontSizes.messageListSubject, ) // thread count is next to subject holder.star.isVisible = appearance.stars holder.starClickArea.isVisible = appearance.stars holder.starClickArea.setOnClickListener(starClickListener) applyDensityValue(holder, appearance.density) view.tag = holder return holder } private fun applyDensityValue(holder: MessageViewHolder, density: UiDensity) { val verticalPadding: Int val textViewMarginTop: Int val lineSpacingMultiplier: Float when (density) { UiDensity.Compact -> { verticalPadding = compactVerticalPadding textViewMarginTop = compactTextViewMarginTop lineSpacingMultiplier = compactLineSpacingMultiplier } UiDensity.Default -> { verticalPadding = defaultVerticalPadding textViewMarginTop = defaultTextViewMarginTop lineSpacingMultiplier = defaultLineSpacingMultiplier } UiDensity.Relaxed -> { verticalPadding = relaxedVerticalPadding textViewMarginTop = relaxedTextViewMarginTop lineSpacingMultiplier = relaxedLineSpacingMultiplier } } holder.itemView.findViewById<Guideline>(R.id.top_guideline).setGuidelineBegin(verticalPadding) holder.itemView.findViewById<Guideline>(R.id.bottom_guideline).setGuidelineEnd(verticalPadding) holder.preview.apply { setMarginTop(textViewMarginTop) setLineSpacing(lineSpacingExtra, lineSpacingMultiplier) } } private fun createFooterViewHolder(parent: ViewGroup): MessageListViewHolder { val view = layoutInflater.inflate(R.layout.message_list_item_footer, parent, false) view.setOnClickListener(footerClickListener) return FooterViewHolder(view) } private fun createMessageViewHolder(parent: ViewGroup?): MessageViewHolder = MessageViewHolder.create( layoutInflater = layoutInflater, parent = parent, appearance = appearance, res = res, contactsPictureLoader = contactsPictureLoader, relativeDateTimeFormatter = relativeDateTimeFormatter, colors = colors, theme = theme, onClickListener = messageClickedListener, onLongClickListener = messageLongClickedListener, contactPictureContainerClickListener = contactPictureContainerClickListener, starClickListener = starClickListener, ) override fun onBindViewHolder(holder: MessageListViewHolder, position: Int) { when (val viewType = getItemViewType(position)) { TYPE_MESSAGE -> { val messageListItem = getItem(position) bindMessageViewHolder(holder as MessageViewHolder, messageListItem) val messageViewHolder = holder as MessageViewHolder messageViewHolder.bind( messageListItem = messageListItem, isActive = isActiveMessage(messageListItem), isSelected = isSelected(messageListItem), ) } TYPE_FOOTER -> { bindFooterViewHolder(holder as FooterViewHolder) val footerViewHolder = holder as FooterViewHolder footerViewHolder.bind(footerText) } else -> { Loading @@ -366,207 +252,6 @@ class MessageListAdapter internal constructor( } } private fun bindMessageViewHolder(holder: MessageViewHolder, messageListItem: MessageListItem) { val isSelected = selected.contains(messageListItem.uniqueId) val isActive = isActiveMessage(messageListItem) if (appearance.showContactPicture) { holder.contactPictureClickArea.isSelected = isSelected if (isSelected) { holder.contactPicture.isVisible = false holder.selected.isVisible = true } else { holder.selected.isVisible = false holder.contactPicture.isVisible = true } holder.contactPictureClickArea.contentDescription = if (isSelected) { res.getString(R.string.swipe_action_deselect) } else { res.getString(R.string.swipe_action_select) } } with(messageListItem) { val foregroundColor = selectForegroundColor(isSelected, isRead, isActive) val maybeBoldTypeface = if (isRead) Typeface.NORMAL else Typeface.BOLD val displayDate = relativeDateTimeFormatter.formatDate(messageDate) val displayThreadCount = if (appearance.showingThreadedList) threadCount else 0 val subject = MlfUtils.buildSubject(subject, res.getString(R.string.general_no_subject), displayThreadCount) if (appearance.showAccountChip) { val accountChipDrawable = holder.chip.drawable.mutate() DrawableCompat.setTint(accountChipDrawable, account.chipColor) holder.chip.setImageDrawable(accountChipDrawable) } if (appearance.stars) { holder.star.isSelected = isStarred if (isStarred) { holder.star.clearColorFilter() } else { holder.star.setColorFilter(foregroundColor) } holder.starClickArea.contentDescription = if (isStarred) { res.getString(R.string.unflag_action) } else { res.getString(R.string.flag_action) } } holder.uniqueId = uniqueId if (appearance.showContactPicture && holder.contactPicture.isVisible) { setContactPicture(holder.contactPicture, displayAddress) } holder.itemView.setBackgroundColor(selectBackgroundColor(isSelected, isRead, isActive)) updateWithThreadCount(holder, displayThreadCount) val beforePreviewText = if (appearance.senderAboveSubject) subject else displayName val messageStringBuilder = SpannableStringBuilder(beforePreviewText) if (appearance.previewLines > 0) { val preview = getPreview(isMessageEncrypted, previewText) if (preview.isNotEmpty()) { messageStringBuilder.append(" – ").append(preview) } } holder.preview.setTextColor(foregroundColor) holder.preview.setText(messageStringBuilder, TextView.BufferType.SPANNABLE) formatPreviewText(holder.preview, beforePreviewText, isRead, isActive, isSelected) holder.subject.typeface = Typeface.create(holder.subject.typeface, maybeBoldTypeface) holder.subject.setTextColor(foregroundColor) val firstLineText = if (appearance.senderAboveSubject) displayName else subject holder.subject.text = firstLineText holder.subject.contentDescription = if (isRead) { null } else { res.getString(R.string.message_list_content_description_unread_prefix, firstLineText) } holder.date.typeface = Typeface.create(holder.date.typeface, maybeBoldTypeface) holder.date.setTextColor(foregroundColor) holder.date.text = displayDate holder.attachment.isVisible = hasAttachments holder.attachment.setColorFilter(foregroundColor) val statusHolder = buildStatusHolder(isForwarded, isAnswered) if (statusHolder != null) { holder.status.setImageDrawable(statusHolder) holder.status.isVisible = true } else { holder.status.isVisible = false } } } private fun bindFooterViewHolder(holder: FooterViewHolder) { holder.text.text = footerText } private fun formatPreviewText( preview: MaterialTextView, beforePreviewText: CharSequence, messageRead: Boolean, active: Boolean, selected: Boolean, ) { val previewText = preview.text as Spannable val textColor = selectPreviewTextColor(active, selected) val beforePreviewLength = beforePreviewText.length addBeforePreviewSpan(previewText, beforePreviewLength, messageRead) previewText.setSpan( ForegroundColorSpan(textColor), beforePreviewLength, previewText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, ) } private fun addBeforePreviewSpan(text: Spannable, length: Int, messageRead: Boolean) { val fontSize = if (appearance.senderAboveSubject) { appearance.fontSizes.messageListSubject } else { appearance.fontSizes.messageListSender } if (fontSize != FontSizes.FONT_DEFAULT) { val span = AbsoluteSizeSpan(fontSize, true) text.setSpan(span, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } if (!messageRead) { val span = StyleSpan(Typeface.BOLD) text.setSpan(span, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } } private fun setContactPicture(contactPictureView: ImageView, displayAddress: Address?) { if (displayAddress != null) { contactsPictureLoader.setContactPicture(contactPictureView, displayAddress) } else { contactPictureView.setImageResource(Icons.Outlined.Check) } } private fun buildStatusHolder(forwarded: Boolean, answered: Boolean): Drawable? { if (forwarded && answered) { return forwardedAnsweredIcon } else if (answered) { return answeredIcon } else if (forwarded) { return forwardedIcon } return null } private fun selectBackgroundColor(selected: Boolean, read: Boolean, active: Boolean): Int { val backGroundAsReadIndicator = appearance.backGroundAsReadIndicator return when { selected -> selectedItemBackgroundColor active -> activeItemBackgroundColor backGroundAsReadIndicator && read -> readItemBackgroundColor backGroundAsReadIndicator && !read -> unreadItemBackgroundColor else -> regularItemBackgroundColor } } private fun selectForegroundColor(selected: Boolean, read: Boolean, active: Boolean): Int { return when { selected -> selectedItemColor active -> activeItemColor read -> readItemColor !read -> unreadItemColor else -> regularItemColor } } private fun selectPreviewTextColor(active: Boolean, selected: Boolean): Int { return when { selected -> previewTextColor active -> previewActiveTextColor else -> previewTextColor } } private fun updateWithThreadCount(holder: MessageViewHolder, threadCount: Int) { if (threadCount > 1) { holder.threadCount.text = String.format("%d", threadCount) holder.threadCount.isVisible = true } else { holder.threadCount.isVisible = false } } private fun getPreview(isMessageEncrypted: Boolean, previewText: String): String { return if (isMessageEncrypted) { res.getString(R.string.preview_encrypted) } else { previewText } } private fun isActiveMessage(item: MessageListItem): Boolean { val activeMessage = this.activeMessage ?: return false Loading Loading @@ -639,12 +324,6 @@ class MessageListAdapter internal constructor( } } private fun Resources.getFloatCompat(@DimenRes resId: Int) = ResourcesCompat.getFloat(this, resId) private fun View.setMarginTop(margin: Int) { (layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin = margin } private class MessageListDiffCallback( private val oldMessageList: List<MessageListItem>, private val newMessageList: List<MessageListItem>, Loading
legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt +1 −0 Original line number Diff line number Diff line Loading @@ -53,6 +53,7 @@ import com.fsck.k9.ui.choosefolder.ChooseFolderActivity import com.fsck.k9.ui.choosefolder.ChooseFolderResultContract import com.fsck.k9.ui.helper.RelativeDateTimeFormatter import com.fsck.k9.ui.messagelist.MessageListFragment.MessageListFragmentListener.Companion.MAX_PROGRESS import com.fsck.k9.ui.messagelist.item.MessageViewHolder import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback import com.google.android.material.snackbar.Snackbar Loading
legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt +1 −0 Original line number Diff line number Diff line Loading @@ -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.MessageViewHolder import com.google.android.material.color.ColorRoles import com.google.android.material.textview.MaterialTextView import kotlin.math.abs Loading
legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListViewHolder.ktdeleted 100644 → 0 +0 −30 Original line number Diff line number Diff line package com.fsck.k9.ui.messagelist import android.view.View import android.widget.ImageView import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.fsck.k9.ui.R import com.google.android.material.textview.MaterialTextView sealed class MessageListViewHolder(view: View) : ViewHolder(view) class MessageViewHolder(view: View) : MessageListViewHolder(view) { var uniqueId: Long = -1L val selected: View = view.findViewById(R.id.selected) val contactPicture: ImageView = view.findViewById(R.id.contact_picture) val contactPictureClickArea: View = view.findViewById(R.id.contact_picture_click_area) val subject: MaterialTextView = view.findViewById(R.id.subject) val preview: MaterialTextView = view.findViewById(R.id.preview) val date: MaterialTextView = view.findViewById(R.id.date) val chip: ImageView = view.findViewById(R.id.account_color_chip) val threadCount: MaterialTextView = view.findViewById(R.id.thread_count) val star: ImageView = view.findViewById(R.id.star) val starClickArea: View = view.findViewById(R.id.star_click_area) val attachment: ImageView = view.findViewById(R.id.attachment) val status: ImageView = view.findViewById(R.id.status) } class FooterViewHolder(view: View) : MessageListViewHolder(view) { val text: MaterialTextView = view.findViewById(R.id.main_text) }
legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MlfUtils.java +1 −1 Original line number Diff line number Diff line Loading @@ -29,7 +29,7 @@ public class MlfUtils { account.setLastSelectedFolderId(folderId); } static String buildSubject(String subjectFromCursor, String emptySubject, int threadCount) { public static String buildSubject(String subjectFromCursor, String emptySubject, int threadCount) { if (TextUtils.isEmpty(subjectFromCursor)) { return emptySubject; } else if (threadCount > 1) { Loading