Add messages sorting
MODIFY
app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt
app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt
@@ -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
messageItemAuthor.run {
- text = if (item.folderId == MessageFolder.SENT.id) item.recipient else item.sender
+ 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)
+ 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
+ messageItemAttachmentIcon.visibility =
+ if (item.hasAttachments) View.VISIBLE else View.GONE
root.setOnClickListener {
- holder.bindingAdapterPosition.let { if (it != NO_POSITION) onClickListener(item, it) }
+ holder.bindingAdapterPosition.let {
+ if (it != NO_POSITION) onItemClickListener(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

ADD
app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabDataItem.kt
+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
+}

MODIFY
app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt
app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt
@@ -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() {

MODIFY
app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt
app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt
@@ -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

MODIFY
app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt
app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt
@@ -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)

MODIFY
app/src/main/java/io/github/wulkanowy/ui/widgets/DividerItemDecoration.kt
app/src/main/java/io/github/wulkanowy/ui/widgets/DividerItemDecoration.kt
@@ -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

ADD
app/src/main/res/layout/item_message_chips.xml
+<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>

MODIFY
app/src/main/res/values/strings.xml
app/src/main/res/values/strings.xml
<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>

MODIFY
app/src/main/res/values/styles.xml
app/src/main/res/values/styles.xml
<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>

Add messages sorting (#1262)