Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 2a04ffa3 authored by Rafael Tonholo's avatar Rafael Tonholo Committed by GitHub
Browse files

Merge pull request #9619 from wmontwe/refactor-message-list-adapter-decouple-logic

Refactor message list adapter to decouple logic
parents 3140d38c c4f55c48
Loading
Loading
Loading
Loading
+30 −351
Original line number Diff line number Diff line
@@ -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

@@ -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,
@@ -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")
@@ -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)
@@ -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 -> {
@@ -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

@@ -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>,
+1 −0
Original line number Diff line number Diff line
@@ -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
+1 −0
Original line number Diff line number Diff line
@@ -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
+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)
}
+1 −1
Original line number Diff line number Diff line
@@ -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