1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2025-01-19 00:26:45 -06:00

Add messages sorting (#1262)

This commit is contained in:
Mateusz Idziejczak 2021-08-04 15:16:54 +02:00 committed by GitHub
parent d73aa605f9
commit b61e63249c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 258 additions and 63 deletions

View File

@ -4,6 +4,8 @@ import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
@ -11,60 +13,114 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.databinding.ItemMessageBinding
import io.github.wulkanowy.databinding.ItemMessageChipsBinding
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
class MessageTabAdapter @Inject constructor() :
RecyclerView.Adapter<MessageTabAdapter.ItemViewHolder>() {
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var onClickListener: (Message, position: Int) -> Unit = { _, _ -> }
enum class ViewType { HEADER, ITEM }
var onItemClickListener: (Message, position: Int) -> Unit = { _, _ -> }
var onHeaderClickListener: (chip: CompoundButton, isChecked: Boolean) -> Unit = { _, _ -> }
var onChangesDetectedListener = {}
private var items = mutableListOf<Message>()
private var items = mutableListOf<MessageTabDataItem>()
private var onlyUnread: Boolean? = null
private var onlyWithAttachments = false
fun setDataItems(data: List<Message>) {
fun setDataItems(
data: List<MessageTabDataItem>,
onlyUnread: Boolean?,
onlyWithAttachments: Boolean
) {
if (items.size != data.size) onChangesDetectedListener()
val diffResult = DiffUtil.calculateDiff(MessageTabDiffUtil(items, data))
items = data.toMutableList()
this.onlyUnread = onlyUnread
this.onlyWithAttachments = onlyWithAttachments
diffResult.dispatchUpdatesTo(this)
}
override fun getItemViewType(position: Int): Int {
return when (position) {
0 -> ViewType.HEADER.ordinal
else -> ViewType.ITEM.ordinal
}
}
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemMessageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ViewType.ITEM.ordinal -> ItemViewHolder(
ItemMessageBinding.inflate(inflater, parent, false)
)
ViewType.HEADER.ordinal -> HeaderViewHolder(
ItemMessageChipsBinding.inflate(inflater, parent, false)
)
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = items[position]
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ItemViewHolder -> {
val item = (items[position] as MessageTabDataItem.MessageItem).message
with(holder.binding) {
val style = if (item.unread) Typeface.BOLD else Typeface.NORMAL
with(holder.binding) {
val style = if (item.unread) Typeface.BOLD else Typeface.NORMAL
messageItemAuthor.run {
text = if (item.folderId == MessageFolder.SENT.id) item.recipient else item.sender
setTypeface(null, style)
messageItemAuthor.run {
text =
if (item.folderId == MessageFolder.SENT.id) item.recipient else item.sender
setTypeface(null, style)
}
messageItemSubject.run {
text =
if (item.subject.isNotBlank()) item.subject else context.getString(R.string.message_no_subject)
setTypeface(null, style)
}
messageItemDate.run {
text = item.date.toFormattedString()
setTypeface(null, style)
}
messageItemAttachmentIcon.visibility =
if (item.hasAttachments) View.VISIBLE else View.GONE
root.setOnClickListener {
holder.bindingAdapterPosition.let {
if (it != NO_POSITION) onItemClickListener(item, it)
}
}
}
}
messageItemSubject.run {
text = if (item.subject.isNotBlank()) item.subject else context.getString(R.string.message_no_subject)
setTypeface(null, style)
}
messageItemDate.run {
text = item.date.toFormattedString()
setTypeface(null, style)
}
messageItemAttachmentIcon.visibility = if (item.hasAttachments) View.VISIBLE else View.GONE
root.setOnClickListener {
holder.bindingAdapterPosition.let { if (it != NO_POSITION) onClickListener(item, it) }
is HeaderViewHolder -> {
with(holder.binding) {
if (onlyUnread == null) chipUnread.isVisible = false
else {
chipUnread.isVisible = true
chipUnread.isChecked = onlyUnread!!
chipUnread.setOnCheckedChangeListener(onHeaderClickListener)
}
chipAttachments.isChecked = onlyWithAttachments
chipAttachments.setOnCheckedChangeListener(onHeaderClickListener)
}
}
}
}
class ItemViewHolder(val binding: ItemMessageBinding) : RecyclerView.ViewHolder(binding.root)
class HeaderViewHolder(val binding: ItemMessageChipsBinding) :
RecyclerView.ViewHolder(binding.root)
private class MessageTabDiffUtil(private val old: List<Message>, private val new: List<Message>) :
private class MessageTabDiffUtil(
private val old: List<MessageTabDataItem>,
private val new: List<MessageTabDataItem>
) :
DiffUtil.Callback() {
override fun getOldListSize(): Int = old.size

View File

@ -0,0 +1,15 @@
package io.github.wulkanowy.ui.modules.message.tab
import io.github.wulkanowy.data.db.entities.Message
sealed class MessageTabDataItem {
data class MessageItem(val message: Message) : MessageTabDataItem() {
override val id = message.id
}
object Header : MessageTabDataItem() {
override val id = Long.MIN_VALUE
}
abstract val id: Long
}

View File

@ -7,6 +7,7 @@ import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.widget.CompoundButton
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
@ -48,6 +49,10 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
override val isViewEmpty
get() = tabAdapter.itemCount == 0
override var onlyUnread: Boolean? = false
override var onlyWithAttachments = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@ -58,26 +63,33 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
super.onViewCreated(view, savedInstanceState)
binding = FragmentMessageTabBinding.bind(view)
messageContainer = binding.messageTabRecycler
presenter.onAttachView(this, MessageFolder.valueOf(
(savedInstanceState ?: arguments)?.getString(MESSAGE_TAB_FOLDER_ID).orEmpty()
))
val folder = MessageFolder.valueOf(
(savedInstanceState ?: requireArguments()).getString(MESSAGE_TAB_FOLDER_ID).orEmpty()
)
presenter.onAttachView(this, folder)
}
override fun initView() {
with(tabAdapter) {
onClickListener = presenter::onMessageItemSelected
onItemClickListener = presenter::onMessageItemSelected
onHeaderClickListener = ::onChipChecked
onChangesDetectedListener = ::resetListPosition
}
with(binding.messageTabRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = tabAdapter
addItemDecoration(DividerItemDecoration(context))
addItemDecoration(DividerItemDecoration(context, false))
}
with(binding) {
messageTabSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
messageTabSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
messageTabSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
messageTabSwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor(
R.attr.colorSwipeRefresh
)
)
messageTabErrorRetry.setOnClickListener { presenter.onRetry() }
messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
@ -99,8 +111,9 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
})
}
override fun updateData(data: List<Message>) {
tabAdapter.setDataItems(data)
override fun updateData(data: List<MessageTabDataItem>, hide: Boolean) {
if (hide) onlyUnread = null
tabAdapter.setDataItems(data, onlyUnread, onlyWithAttachments)
}
override fun showProgress(show: Boolean) {
@ -143,8 +156,19 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
(parentFragment as? MessageFragment)?.onChildFragmentLoaded()
}
fun onParentLoadData(forceRefresh: Boolean) {
presenter.onParentViewLoadData(forceRefresh)
fun onParentLoadData(
forceRefresh: Boolean,
onlyUnread: Boolean? = this.onlyUnread,
onlyWithAttachments: Boolean = this.onlyWithAttachments
) {
presenter.onParentViewLoadData(forceRefresh, onlyUnread, onlyWithAttachments)
}
private fun onChipChecked(chip: CompoundButton, isChecked: Boolean) {
when (chip.id) {
R.id.chip_unread -> presenter.onUnreadFilterSelected(isChecked)
R.id.chip_attachments -> presenter.onAttachmentsFilterSelected(isChecked)
}
}
fun onParentDeleteMessage() {

View File

@ -55,15 +55,15 @@ class MessageTabPresenter @Inject constructor(
fun onSwipeRefresh() {
Timber.i("Force refreshing the $folder message")
onParentViewLoadData(true)
view?.run { onParentViewLoadData(true, onlyUnread, onlyWithAttachments) }
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
loadData(true, onlyUnread == true, onlyWithAttachments)
}
loadData(true)
}
fun onDetailsClick() {
@ -71,11 +71,15 @@ class MessageTabPresenter @Inject constructor(
}
fun onDeleteMessage() {
loadData(true)
view?.run { loadData(true, onlyUnread == true, onlyWithAttachments) }
}
fun onParentViewLoadData(forceRefresh: Boolean) {
loadData(forceRefresh)
fun onParentViewLoadData(
forceRefresh: Boolean,
onlyUnread: Boolean? = view?.onlyUnread,
onlyWithAttachments: Boolean = view?.onlyWithAttachments == true
) {
loadData(forceRefresh, onlyUnread == true, onlyWithAttachments)
}
fun onMessageItemSelected(message: Message, position: Int) {
@ -83,7 +87,25 @@ class MessageTabPresenter @Inject constructor(
view?.openMessage(message)
}
private fun loadData(forceRefresh: Boolean) {
fun onUnreadFilterSelected(isChecked: Boolean) {
view?.run {
onlyUnread = isChecked
onParentViewLoadData(false, onlyUnread, onlyWithAttachments)
}
}
fun onAttachmentsFilterSelected(isChecked: Boolean) {
view?.run {
onlyWithAttachments = isChecked
onParentViewLoadData(false, onlyUnread, onlyWithAttachments)
}
}
private fun loadData(
forceRefresh: Boolean,
onlyUnread: Boolean,
onlyWithAttachments: Boolean
) {
Timber.i("Loading $folder message data started")
flowWithResourceIn {
@ -100,7 +122,15 @@ class MessageTabPresenter @Inject constructor(
showProgress(false)
showContent(true)
messages = it.data
updateData(getFilteredData(lastSearchQuery))
val filteredData = getFilteredData(
lastSearchQuery,
onlyUnread,
onlyWithAttachments
)
val newItems = listOf(MessageTabDataItem.Header) + filteredData.map {
MessageTabDataItem.MessageItem(it)
}
updateData(newItems, folder.id == MessageFolder.SENT.id)
notifyParentDataLoaded()
}
}
@ -108,7 +138,7 @@ class MessageTabPresenter @Inject constructor(
Status.SUCCESS -> {
Timber.i("Loading $folder message result: Success")
messages = it.data!!
updateData(getFilteredData(lastSearchQuery))
updateData(getFilteredData(lastSearchQuery, onlyUnread, onlyWithAttachments))
analytics.logEvent(
"load_data",
"type" to "messages",
@ -166,24 +196,42 @@ class MessageTabPresenter @Inject constructor(
}
}
private fun getFilteredData(query: String): List<Message> {
return if (query.trim().isEmpty()) {
messages.sortedByDescending { it.date }
private fun getFilteredData(
query: String,
onlyUnread: Boolean = false,
onlyWithAttachments: Boolean = false
): List<Message> {
if (query.trim().isEmpty()) {
val sortedMessages = messages.sortedByDescending { it.date }
return when {
onlyUnread && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments }
onlyUnread -> sortedMessages.filter { it.unread == onlyUnread }
onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments }
else -> sortedMessages
}
} else {
messages
val sortedMessages = messages
.map { it to calculateMatchRatio(it, query) }
.sortedByDescending { it.second }
.filter { it.second > 5000 }
.sortedWith(compareBy<Pair<Message, Int>> { -it.second }.thenByDescending { it.first.date })
.filter { it.second > 6000 }
.map { it.first }
return when {
onlyUnread && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments }
onlyUnread -> sortedMessages.filter { it.unread == onlyUnread }
onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments }
else -> sortedMessages
}
}
}
private fun updateData(data: List<Message>) {
view?.run {
showEmpty(data.isEmpty())
showContent(data.isNotEmpty())
showContent(true)
showErrorView(false)
updateData(data)
val newItems =
listOf(MessageTabDataItem.Header) + data.map { MessageTabDataItem.MessageItem(it) }
updateData(newItems, folder.id == MessageFolder.SENT.id)
}
}
@ -204,14 +252,6 @@ class MessageTabPresenter @Inject constructor(
FuzzySearch.ratio(
query.lowercase(),
message.date.toFormattedString("dd.MM.yyyy").lowercase()
),
FuzzySearch.ratio(
query.lowercase(),
message.date.toFormattedString("d MMMM").lowercase()
),
FuzzySearch.ratio(
query.lowercase(),
message.date.toFormattedString("d MMMM yyyy").lowercase()
)
).maxOrNull() ?: 0

View File

@ -7,11 +7,15 @@ interface MessageTabView : BaseView {
val isViewEmpty: Boolean
var onlyUnread: Boolean?
var onlyWithAttachments: Boolean
fun initView()
fun resetListPosition()
fun updateData(data: List<Message>)
fun updateData(data: List<MessageTabDataItem>, hide: Boolean)
fun showProgress(show: Boolean)

View File

@ -5,7 +5,10 @@ import android.graphics.Canvas
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
class DividerItemDecoration(context: Context) : DividerItemDecoration(context, VERTICAL) {
class DividerItemDecoration(
context: Context,
private val showDividerWithFirstItem: Boolean = true
) : DividerItemDecoration(context, VERTICAL) {
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
canvas.save()
@ -13,6 +16,8 @@ class DividerItemDecoration(context: Context) : DividerItemDecoration(context, V
val dividerRight = parent.width - parent.paddingRight
for (i in 0..parent.childCount - 2) {
if (!showDividerWithFirstItem && i == 0) continue
val child = parent.getChildAt(i)
val params = child.layoutParams as RecyclerView.LayoutParams
val dividerTop = child.bottom + params.bottomMargin

View File

@ -0,0 +1,40 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/messageChipsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingTop="10dp"
android:paddingRight="16dp"
tools:context=".ui.modules.message.tab.MessageTabAdapter">
<com.google.android.material.chip.ChipGroup
android:id="@+id/messageChipGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.chip.Chip
android:id="@+id/chip_unread"
style="@style/Widget.MaterialComponents.Chip.Choice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/message_chip_only_unread"
app:checkedIcon="@drawable/ic_mtrl_chip_checked_black"
app:checkedIconEnabled="true"
app:checkedIconTint="@color/mtrl_choice_chip_text_color" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_attachments"
style="@style/Widget.MaterialComponents.Chip.Choice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/message_chip_only_with_attachments"
app:checkedIcon="@drawable/ic_mtrl_chip_checked_black"
app:checkedIconEnabled="true"
app:checkedIconTint="@color/mtrl_choice_chip_text_color" />
</com.google.android.material.chip.ChipGroup>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -234,6 +234,8 @@
<string name="message_not_exists">Message does not exist</string>
<string name="message_required_recipients">You need to choose at least 1 recipient</string>
<string name="message_content_min_length">The message content must be at least 3 characters</string>
<string name="message_chip_only_unread">Only unread</string>
<string name="message_chip_only_with_attachments">Only with attachments</string>
<plurals name="message_number_item">
<item quantity="one">%d message</item>
<item quantity="other">%d messages</item>

View File

@ -36,6 +36,15 @@
<item name="android:textColor">?android:textColorPrimary</item>
</style>
<style name="Widget.Wulkanowy.Chip.Choice" parent="Widget.MaterialComponents.Chip.Choice">
...
<item name="materialThemeOverlay">@style/ThemeOverlay.Wulkanowy.Chip.Choice</item>
</style>
<style name="ThemeOverlay.Wulkanowy.Chip.Choice" parent="">
<item name="elevationOverlayEnabled">false</item>
</style>
<style name="WulkanowyTheme.TextAppearanceBottomNavigation">
<item name="android:textSize">11sp</item>
</style>