diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt index bb12c8df0..571cc6d55 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt +++ b/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() { + RecyclerView.Adapter() { - 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() + private var items = mutableListOf() + private var onlyUnread: Boolean? = null + private var onlyWithAttachments = false - fun setDataItems(data: List) { + fun setDataItems( + data: List, + 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, private val new: List) : + private class MessageTabDiffUtil( + private val old: List, + private val new: List + ) : DiffUtil.Callback() { override fun getOldListSize(): Int = old.size diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabDataItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabDataItem.kt new file mode 100644 index 000000000..4f51a936f --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabDataItem.kt @@ -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 +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt index f08059f2c..dfd95b721 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt +++ b/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(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(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(R.layout.frag }) } - override fun updateData(data: List) { - tabAdapter.setDataItems(data) + override fun updateData(data: List, hide: Boolean) { + if (hide) onlyUnread = null + tabAdapter.setDataItems(data, onlyUnread, onlyWithAttachments) } override fun showProgress(show: Boolean) { @@ -143,8 +156,19 @@ class MessageTabFragment : BaseFragment(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() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index 0f4e86337..cf1811822 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/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 { - return if (query.trim().isEmpty()) { - messages.sortedByDescending { it.date } + private fun getFilteredData( + query: String, + onlyUnread: Boolean = false, + onlyWithAttachments: Boolean = false + ): List { + 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> { -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) { 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 diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt index fe9b60077..a856da3bc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt +++ b/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) + fun updateData(data: List, hide: Boolean) fun showProgress(show: Boolean) diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/DividerItemDecoration.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/DividerItemDecoration.kt index ca2bda268..933301317 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/DividerItemDecoration.kt +++ b/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 diff --git a/app/src/main/res/layout/item_message_chips.xml b/app/src/main/res/layout/item_message_chips.xml new file mode 100644 index 000000000..481a94835 --- /dev/null +++ b/app/src/main/res/layout/item_message_chips.xml @@ -0,0 +1,40 @@ + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4cf18a666..c6323668a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -234,6 +234,8 @@ Message does not exist You need to choose at least 1 recipient The message content must be at least 3 characters + Only unread + Only with attachments %d message %d messages diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index c811e9777..628aa2974 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -36,6 +36,15 @@ ?android:textColorPrimary + + + +