1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-20 04:19:08 -05:00

Add option to select multiple messages to delete (#1780)

This commit is contained in:
Rafał Borcz 2022-03-28 19:30:20 +02:00 committed by GitHub
parent 63380d3e12
commit 2131e892ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 569 additions and 238 deletions

View File

@ -103,7 +103,7 @@ class MessageRepository @Inject constructor(
message: Message,
markAsRead: Boolean = false,
): Flow<Resource<MessageWithAttachment?>> = networkBoundResource(
isResultEmpty = { it == null },
isResultEmpty = { it?.message?.content.isNullOrBlank() },
shouldFetch = {
checkNotNull(it) { "This message no longer exist!" }
Timber.d("Message content in db empty: ${it.message.content.isEmpty()}")
@ -151,20 +151,27 @@ class MessageRepository @Inject constructor(
recipients = recipients.mapFromEntities()
)
suspend fun deleteMessage(student: Student, message: Message) {
val isDeleted = sdk.init(student).deleteMessages(
messages = listOf(message.messageId), message.folderId
)
suspend fun deleteMessages(student: Student, messages: List<Message>) {
val folderId = messages.first().folderId
val isDeleted = sdk.init(student)
.deleteMessages(messages = messages.map { it.messageId }, folderId = folderId)
if (message.folderId != MessageFolder.TRASHED.id && isDeleted) {
val deletedMessage = message.copy(folderId = MessageFolder.TRASHED.id).apply {
id = message.id
content = message.content
if (folderId != MessageFolder.TRASHED.id && isDeleted) {
val deletedMessages = messages.map {
it.copy(folderId = MessageFolder.TRASHED.id)
.apply {
id = it.id
content = it.content
}
messagesDb.updateAll(listOf(deletedMessage))
} else messagesDb.deleteAll(listOf(message))
}
messagesDb.updateAll(deletedMessages)
} else messagesDb.deleteAll(messages)
}
suspend fun deleteMessage(student: Student, message: Message) =
deleteMessages(student, listOf(message))
var draftMessage: MessageDraft?
get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft))
?.let { json.decodeFromString(it) }

View File

@ -2,14 +2,8 @@ package io.github.wulkanowy.ui.modules.attendance
import android.content.DialogInterface.BUTTON_POSITIVE
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.*
import android.view.View.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible
@ -68,7 +62,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
private val actionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val inflater = mode.menuInflater
inflater.inflate(R.menu.context_menu_excuse, menu)
inflater.inflate(R.menu.context_menu_attendance, menu)
return true
}

View File

@ -4,12 +4,14 @@ import android.os.Bundle
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.enums.MessageFolder.SENT
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
import io.github.wulkanowy.data.enums.MessageFolder.*
import io.github.wulkanowy.databinding.FragmentMessageBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
@ -78,7 +80,6 @@ class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_m
}
binding.messageTabLayout.elevation = requireContext().dpToPx(4f)
binding.openSendMessageButton.setOnClickListener { presenter.onSendMessageButtonClicked() }
}
@ -93,12 +94,37 @@ class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_m
binding.messageProgress.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showNewMessage(show: Boolean) {
binding.openSendMessageButton.run {
if (show) show() else hide()
}
}
override fun showTabLayout(show: Boolean) {
binding.messageTabLayout.isVisible = show
with(binding.messageViewPager) {
isUserInputEnabled = show
updateLayoutParams<ViewGroup.MarginLayoutParams> {
updateMargins(top = if (show) requireContext().dpToPx(48f).toInt() else 0)
}
}
}
fun onChildFragmentShowActionMode(show: Boolean) {
presenter.onChildViewShowActionMode(show)
}
fun onChildFragmentLoaded() {
presenter.onChildViewLoaded()
}
override fun notifyChildMessageDeleted(tabId: Int) {
(pagerAdapter.getFragmentInstance(tabId) as? MessageTabFragment)?.onParentDeleteMessage()
fun onChildFragmentShowNewMessage(show: Boolean) {
presenter.onChildViewShowNewMessage(show)
}
fun onFragmentChanged() {
presenter.onFragmentChanged()
}
override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) {
@ -106,6 +132,13 @@ class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_m
?.onParentLoadData(forceRefresh)
}
override fun notifyChildrenFinishActionMode() {
repeat(3) {
(pagerAdapter.getFragmentInstance(it) as? MessageTabFragment)
?.onParentFinishActionMode()
}
}
override fun openSendMessage() {
context?.let { it.startActivity(SendMessageActivity.getStartIntent(it)) }
}

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.message
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -24,6 +23,7 @@ class MessagePresenter @Inject constructor(
fun onPageSelected(index: Int) {
loadChild(index)
view?.notifyChildrenFinishActionMode()
}
private fun loadData() {
@ -35,6 +35,10 @@ class MessagePresenter @Inject constructor(
view?.notifyChildLoadData(index, forceRefresh)
}
fun onFragmentChanged() {
view?.notifyChildrenFinishActionMode()
}
fun onChildViewLoaded() {
view?.apply {
showContent(true)
@ -42,6 +46,14 @@ class MessagePresenter @Inject constructor(
}
}
fun onChildViewShowNewMessage(show: Boolean) {
view?.showNewMessage(show)
}
fun onChildViewShowActionMode(show: Boolean) {
view?.showTabLayout(!show)
}
fun onSendMessageButtonClicked() {
view?.openSendMessage()
}

View File

@ -12,9 +12,13 @@ interface MessageView : BaseView {
fun showProgress(show: Boolean)
fun showNewMessage(show: Boolean)
fun showTabLayout(show: Boolean)
fun notifyChildLoadData(index: Int, forceRefresh: Boolean)
fun notifyChildMessageDeleted(tabId: Int)
fun notifyChildrenFinishActionMode()
fun openSendMessage()
}

View File

@ -142,7 +142,7 @@ class MessagePreviewFragment :
}
override fun setNotDeletedOptionsLabels() {
menuDeleteButton?.setTitle(R.string.message_move_to_bin)
menuDeleteButton?.setTitle(R.string.message_move_to_trash)
}
override fun showErrorView(show: Boolean) {

View File

@ -2,15 +2,12 @@ package io.github.wulkanowy.ui.modules.message.tab
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
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
@ -20,118 +17,141 @@ import javax.inject.Inject
class MessageTabAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
enum class ViewType { HEADER, ITEM }
var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit = { _, _ -> }
var onItemClickListener: (Message, position: Int) -> Unit = { _, _ -> }
var onHeaderClickListener: (chip: CompoundButton, isChecked: Boolean) -> Unit = { _, _ -> }
var onLongItemClickListener: (MessageTabDataItem.MessageItem) -> Unit = {}
var onHeaderClickListener: (CompoundButton, Boolean) -> Unit = { _, _ -> }
var onChangesDetectedListener = {}
private var items = mutableListOf<MessageTabDataItem>()
private var onlyUnread: Boolean? = null
private var onlyWithAttachments = false
fun setDataItems(
data: List<MessageTabDataItem>,
onlyUnread: Boolean?,
onlyWithAttachments: Boolean
) {
if (items.size != data.size) onChangesDetectedListener()
fun submitData(data: List<MessageTabDataItem>) {
val originalMessagesSize = items.count { it.viewType == MessageItemViewType.MESSAGE }
val newMessagesSize = data.count { it.viewType == MessageItemViewType.MESSAGE }
if (originalMessagesSize != newMessagesSize) 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 getItemViewType(position: Int) = items[position].viewType.ordinal
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ViewType.ITEM.ordinal -> ItemViewHolder(
return when (MessageItemViewType.values()[viewType]) {
MessageItemViewType.MESSAGE -> ItemViewHolder(
ItemMessageBinding.inflate(inflater, parent, false)
)
ViewType.HEADER.ordinal -> HeaderViewHolder(
MessageItemViewType.FILTERS -> HeaderViewHolder(
ItemMessageChipsBinding.inflate(inflater, parent, false)
)
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ItemViewHolder -> {
val item = (items[position] as MessageTabDataItem.MessageItem).message
is ItemViewHolder -> bindItemViewHolder(holder, position)
is HeaderViewHolder -> bindHeaderViewHolder(holder, position)
}
}
private fun bindHeaderViewHolder(holder: HeaderViewHolder, position: Int) {
val item = items[position] as MessageTabDataItem.FilterHeader
with(holder.binding) {
val style = if (item.unread) Typeface.BOLD else Typeface.NORMAL
if (item.onlyUnread == null) {
chipUnread.isVisible = false
} else {
chipUnread.isVisible = true
chipUnread.isChecked = item.onlyUnread
chipUnread.setOnCheckedChangeListener(onHeaderClickListener)
}
chipUnread.isEnabled = item.isEnabled
chipAttachments.isEnabled = item.isEnabled
chipAttachments.isChecked = item.onlyWithAttachments
chipAttachments.setOnCheckedChangeListener(onHeaderClickListener)
}
}
private fun bindItemViewHolder(holder: ItemViewHolder, position: Int) {
val item = (items[position] as MessageTabDataItem.MessageItem)
val message = item.message
with(holder.binding) {
val style = if (message.unread) Typeface.BOLD else Typeface.NORMAL
messageItemAuthor.run {
text =
if (item.folderId == MessageFolder.SENT.id) item.recipient else item.sender
text = if (message.folderId == MessageFolder.SENT.id) {
message.recipient
} else {
message.sender
}
setTypeface(null, style)
}
messageItemSubject.run {
text =
if (item.subject.isNotBlank()) item.subject else context.getString(R.string.message_no_subject)
text = message.subject.ifBlank { context.getString(R.string.message_no_subject) }
setTypeface(null, style)
}
messageItemDate.run {
text = item.date.toFormattedString()
text = message.date.toFormattedString()
setTypeface(null, style)
}
messageItemAttachmentIcon.visibility =
if (item.hasAttachments) View.VISIBLE else View.GONE
messageItemAttachmentIcon.isVisible = message.hasAttachments
root.setOnClickListener {
holder.bindingAdapterPosition.let {
if (it != NO_POSITION) onItemClickListener(item, it)
if (it != RecyclerView.NO_POSITION) {
onItemClickListener(item, it)
}
}
}
root.setOnLongClickListener {
onLongItemClickListener(item)
return@setOnLongClickListener true
}
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)
}
with(messageItemCheckbox) {
isChecked = item.isSelected
isVisible = item.isActionMode
}
}
}
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<MessageTabDataItem>,
private val new: List<MessageTabDataItem>
) :
DiffUtil.Callback() {
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = old.size
override fun getNewListSize(): Int = new.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return old[oldItemPosition].id == new[newItemPosition].id
val oldItem = old[oldItemPosition]
val newItem = new[newItemPosition]
return if (oldItem is MessageTabDataItem.MessageItem && newItem is MessageTabDataItem.MessageItem) {
oldItem.message.id == newItem.message.id
} else {
oldItem.viewType == newItem.viewType
}
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return old[oldItemPosition] == new[newItemPosition]
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
old[oldItemPosition] == new[newItemPosition]
}
}

View File

@ -2,14 +2,19 @@ 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
sealed class MessageTabDataItem(val viewType: MessageItemViewType) {
data class MessageItem(
val message: Message,
val isSelected: Boolean,
val isActionMode: Boolean
) : MessageTabDataItem(MessageItemViewType.MESSAGE)
data class FilterHeader(
val onlyUnread: Boolean?,
val onlyWithAttachments: Boolean,
val isEnabled: Boolean
) : MessageTabDataItem(MessageItemViewType.FILTERS)
}
object Header : MessageTabDataItem() {
override val id = Long.MIN_VALUE
}
abstract val id: Long
}
enum class MessageItemViewType { FILTERS, MESSAGE }

View File

@ -3,12 +3,13 @@ package io.github.wulkanowy.ui.modules.message.tab
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.View.*
import android.widget.CompoundButton
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -20,7 +21,9 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.hideSoftInput
import javax.inject.Inject
@AndroidEntryPoint
@ -31,9 +34,10 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
lateinit var presenter: MessageTabPresenter
@Inject
lateinit var tabAdapter: MessageTabAdapter
lateinit var messageTabAdapter: MessageTabAdapter
companion object {
const val MESSAGE_TAB_FOLDER_ID = "message_tab_folder_id"
fun newInstance(folder: MessageFolder): MessageTabFragment {
@ -46,11 +50,38 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
}
override val isViewEmpty
get() = tabAdapter.itemCount == 0
get() = messageTabAdapter.itemCount == 0
override var onlyUnread: Boolean? = false
private var actionMode: ActionMode? = null
override var onlyWithAttachments = false
private val actionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val inflater = mode.menuInflater
inflater.inflate(R.menu.context_menu_message_tab, menu)
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
if (presenter.folder == MessageFolder.TRASHED) {
val menuItem = menu.findItem(R.id.messageTabContextMenuDelete)
menuItem.setTitle(R.string.message_delete_forever)
}
return presenter.onPrepareActionMode()
}
override fun onDestroyActionMode(mode: ActionMode) {
presenter.onDestroyActionMode()
actionMode = null
}
override fun onActionItemClicked(mode: ActionMode, menu: MenuItem): Boolean {
when (menu.itemId) {
R.id.messageTabContextMenuDelete -> presenter.onActionModeSelectDelete()
R.id.messageTabContextMenuSelectAll -> presenter.onActionModeSelectCheckAll()
}
return true
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -69,24 +100,25 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
}
override fun initView() {
with(tabAdapter) {
with(messageTabAdapter) {
onItemClickListener = presenter::onMessageItemSelected
onLongItemClickListener = presenter::onMessageItemLongSelected
onHeaderClickListener = ::onChipChecked
onChangesDetectedListener = ::resetListPosition
}
with(binding.messageTabRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = tabAdapter
adapter = messageTabAdapter
addItemDecoration(DividerItemDecoration(context, false))
itemAnimator = null
}
with(binding) {
messageTabSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
messageTabSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
messageTabSwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor(
R.attr.colorSwipeRefresh
)
requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)
)
messageTabErrorRetry.setOnClickListener { presenter.onRetry() }
messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() }
@ -109,9 +141,28 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
})
}
override fun updateData(data: List<MessageTabDataItem>, hide: Boolean) {
if (hide) onlyUnread = null
tabAdapter.setDataItems(data, onlyUnread, onlyWithAttachments)
override fun updateData(data: List<MessageTabDataItem>) {
messageTabAdapter.submitData(data)
}
override fun updateActionModeTitle(selectedMessagesSize: Int) {
actionMode?.title = resources.getQuantityString(
R.plurals.message_selected_messages_count,
selectedMessagesSize,
selectedMessagesSize
)
}
override fun updateSelectAllMenu(isAllSelected: Boolean) {
val menuItem = actionMode?.menu?.findItem(R.id.messageTabContextMenuSelectAll) ?: return
if (isAllSelected) {
menuItem.setTitle(R.string.message_unselect_all)
menuItem.setIcon(R.drawable.ic_message_unselect_all)
} else {
menuItem.setTitle(R.string.message_select_all)
menuItem.setIcon(R.drawable.ic_message_select_all)
}
}
override fun showProgress(show: Boolean) {
@ -146,6 +197,14 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
binding.messageTabSwipe.isRefreshing = show
}
override fun showMessagesDeleted() {
showMessage(getString(R.string.message_messages_deleted))
}
override fun notifyParentShowNewMessage(show: Boolean) {
(parentFragment as? MessageFragment)?.onChildFragmentShowNewMessage(show)
}
override fun openMessage(message: Message) {
(activity as? MainActivity)?.pushView(MessagePreviewFragment.newInstance(message))
}
@ -154,12 +213,16 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
(parentFragment as? MessageFragment)?.onChildFragmentLoaded()
}
fun onParentLoadData(
forceRefresh: Boolean,
onlyUnread: Boolean? = this.onlyUnread,
onlyWithAttachments: Boolean = this.onlyWithAttachments
) {
presenter.onParentViewLoadData(forceRefresh, onlyUnread, onlyWithAttachments)
override fun notifyParentShowActionMode(show: Boolean) {
(parentFragment as? MessageFragment)?.onChildFragmentShowActionMode(show)
}
fun onParentLoadData(forceRefresh: Boolean) {
presenter.onParentViewLoadData(forceRefresh)
}
fun onParentFinishActionMode() {
presenter.onParentFinishActionMode()
}
private fun onChipChecked(chip: CompoundButton, isChecked: Boolean) {
@ -169,8 +232,22 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
}
}
fun onParentDeleteMessage() {
presenter.onDeleteMessage()
override fun showActionMode(show: Boolean) {
if (show) {
actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback)
} else {
actionMode?.finish()
}
}
override fun showRecyclerBottomPadding(show: Boolean) {
binding.messageTabRecycler.updatePadding(
bottom = if (show) requireContext().dpToPx(64f).toInt() else 0
)
}
override fun hideKeyboard() {
activity?.hideSoftInput()
}
override fun onSaveInstanceState(outState: Bundle) {

View File

@ -12,7 +12,10 @@ import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.xdrop.fuzzywuzzy.FuzzySearch
import timber.log.Timber
@ -37,6 +40,14 @@ class MessageTabPresenter @Inject constructor(
private val searchChannel = Channel<String>()
private val messagesToDelete = mutableSetOf<Message>()
private var onlyUnread: Boolean? = false
private var onlyWithAttachments = false
private var isActionMode = false
fun onAttachView(view: MessageTabView, folder: MessageFolder) {
super.onAttachView(view)
view.initView()
@ -47,14 +58,14 @@ class MessageTabPresenter @Inject constructor(
fun onSwipeRefresh() {
Timber.i("Force refreshing the $folder message")
view?.run { onParentViewLoadData(true, onlyUnread, onlyWithAttachments) }
view?.run { loadData(true) }
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
loadData(true, onlyUnread == true, onlyWithAttachments)
loadData(true)
}
}
@ -62,42 +73,135 @@ class MessageTabPresenter @Inject constructor(
view?.showErrorDetailsDialog(lastError)
}
fun onDeleteMessage() {
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 onParentFinishActionMode() {
view?.showActionMode(false)
}
fun onMessageItemSelected(message: Message, position: Int) {
Timber.i("Select message ${message.id} item (position: $position)")
view?.openMessage(message)
fun onDestroyActionMode() {
isActionMode = false
messagesToDelete.clear()
updateDataInView()
view?.run {
enableSwipe(true)
notifyParentShowNewMessage(true)
notifyParentShowActionMode(false)
showRecyclerBottomPadding(true)
}
}
fun onPrepareActionMode(): Boolean {
isActionMode = true
messagesToDelete.clear()
updateDataInView()
view?.apply {
enableSwipe(false)
notifyParentShowNewMessage(false)
notifyParentShowActionMode(true)
showRecyclerBottomPadding(false)
hideKeyboard()
}
return true
}
fun onActionModeSelectDelete() {
Timber.i("Delete ${messagesToDelete.size} messages)")
val messageList = messagesToDelete.toList()
presenterScope.launch {
view?.run {
showProgress(true)
showContent(false)
showActionMode(false)
}
runCatching {
val student = studentRepository.getCurrentStudent(true)
messageRepository.deleteMessages(student, messageList)
}
.onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessagesDeleted() }
}
}
fun onActionModeSelectCheckAll() {
val messagesToSelect = getFilteredData()
val isAllSelected = messagesToDelete.containsAll(messagesToSelect)
if (isAllSelected) {
messagesToDelete.clear()
view?.showActionMode(false)
} else {
messagesToDelete.addAll(messagesToSelect)
updateDataInView()
}
view?.run {
updateSelectAllMenu(!isAllSelected)
updateActionModeTitle(messagesToDelete.size)
}
}
fun onMessageItemLongSelected(messageItem: MessageTabDataItem.MessageItem) {
if (!isActionMode) {
view?.showActionMode(true)
messagesToDelete.add(messageItem.message)
view?.updateActionModeTitle(messagesToDelete.size)
updateDataInView()
}
}
fun onMessageItemSelected(messageItem: MessageTabDataItem.MessageItem, position: Int) {
Timber.i("Select message ${messageItem.message.id} item (position: $position)")
if (!isActionMode) {
view?.run {
showActionMode(false)
openMessage(messageItem.message)
}
} else {
if (!messageItem.isSelected) {
messagesToDelete.add(messageItem.message)
} else {
messagesToDelete.remove(messageItem.message)
}
if (messagesToDelete.isEmpty()) {
view?.showActionMode(false)
}
val filteredData = getFilteredData()
view?.run {
updateActionModeTitle(messagesToDelete.size)
updateSelectAllMenu(messagesToDelete.containsAll(filteredData))
}
updateDataInView()
}
}
fun onUnreadFilterSelected(isChecked: Boolean) {
view?.run {
onlyUnread = isChecked
onParentViewLoadData(false, onlyUnread, onlyWithAttachments)
loadData(false)
}
}
fun onAttachmentsFilterSelected(isChecked: Boolean) {
view?.run {
onlyWithAttachments = isChecked
onParentViewLoadData(false, onlyUnread, onlyWithAttachments)
loadData(false)
}
}
private fun loadData(
forceRefresh: Boolean,
onlyUnread: Boolean,
onlyWithAttachments: Boolean
) {
private fun loadData(forceRefresh: Boolean) {
Timber.i("Loading $folder message data started")
flatResourceFlow {
@ -106,55 +210,30 @@ class MessageTabPresenter @Inject constructor(
messageRepository.getMessages(student, semester, folder, forceRefresh)
}
.logResourceStatus("load $folder message")
.onEach {
when (it) {
is Resource.Intermediate -> {
if (it.data.isNotEmpty()) {
.onResourceData {
messages = it
val filteredData = getFilteredData()
view?.run {
enableSwipe(true)
showErrorView(false)
showRefresh(true)
showProgress(false)
showContent(true)
messages = it.data
val filteredData = getFilteredData(
lastSearchQuery,
onlyUnread,
onlyWithAttachments
)
val messageItems = filteredData.map { message ->
MessageTabDataItem.MessageItem(message)
showEmpty(filteredData.isEmpty())
}
val messageItemsWithHeader =
listOf(MessageTabDataItem.Header) + messageItems
updateData(
messageItemsWithHeader,
folder.id == MessageFolder.SENT.id
)
notifyParentDataLoaded()
updateDataInView()
}
}
}
is Resource.Success -> {
messages = it.data
updateData(
getFilteredData(
lastSearchQuery,
onlyUnread,
onlyWithAttachments
)
)
.onResourceIntermediate { view?.showRefresh(true) }
.onResourceSuccess {
analytics.logEvent(
"load_data",
"type" to "messages",
"items" to it.data.size,
"items" to it.size,
"folder" to folder.name
)
}
else -> {}
}
}
.onResourceNotLoading {
view?.run {
showRefresh(false)
@ -196,56 +275,71 @@ class MessageTabPresenter @Inject constructor(
.debounce(250)
.map { query ->
lastSearchQuery = query
val isOnlyUnread = view?.onlyUnread == true
val isOnlyWithAttachments = view?.onlyWithAttachments == true
getFilteredData(query, isOnlyUnread, isOnlyWithAttachments)
getFilteredData()
}
.catch { Timber.e(it) }
.collect {
Timber.d("Applying filter. Full list: ${messages.size}, filtered: ${it.size}")
updateData(it)
view?.run {
showEmpty(it.isEmpty())
showContent(true)
showErrorView(false)
}
updateDataInView()
view?.resetListPosition()
}
}
}
private fun getFilteredData(
query: String,
onlyUnread: Boolean = false,
onlyWithAttachments: Boolean = false
): List<Message> {
if (query.trim().isEmpty()) {
private fun getFilteredData(): List<Message> {
if (lastSearchQuery.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 }
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments }
(onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread }
onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments }
else -> sortedMessages
}
} else {
val sortedMessages = messages
.map { it to calculateMatchRatio(it, query) }
.map { it to calculateMatchRatio(it, lastSearchQuery) }
.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 }
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments }
(onlyUnread == true) -> 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(true)
showErrorView(false)
val newItems =
listOf(MessageTabDataItem.Header) + data.map { MessageTabDataItem.MessageItem(it) }
updateData(newItems, folder.id == MessageFolder.SENT.id)
private fun updateDataInView() {
val data = getFilteredData()
val list = buildList {
add(
MessageTabDataItem.FilterHeader(
onlyUnread = onlyUnread.takeIf { folder != MessageFolder.SENT },
onlyWithAttachments = onlyWithAttachments,
isEnabled = !isActionMode
)
)
addAll(data.map { message ->
MessageTabDataItem.MessageItem(
message = message,
isSelected = messagesToDelete.any { it.id == message.id },
isActionMode = isActionMode
)
})
}
view?.updateData(list)
}
private fun calculateMatchRatio(message: Message, query: String): Int {

View File

@ -7,15 +7,15 @@ interface MessageTabView : BaseView {
val isViewEmpty: Boolean
var onlyUnread: Boolean?
var onlyWithAttachments: Boolean
fun initView()
fun resetListPosition()
fun updateData(data: List<MessageTabDataItem>, hide: Boolean)
fun updateData(data: List<MessageTabDataItem>)
fun updateActionModeTitle(selectedMessagesSize: Int)
fun updateSelectAllMenu(isAllSelected: Boolean)
fun showProgress(show: Boolean)
@ -25,8 +25,12 @@ interface MessageTabView : BaseView {
fun showEmpty(show: Boolean)
fun showMessagesDeleted()
fun showErrorView(show: Boolean)
fun notifyParentShowNewMessage(show: Boolean)
fun setErrorDetails(message: String)
fun showRefresh(show: Boolean)
@ -34,4 +38,12 @@ interface MessageTabView : BaseView {
fun openMessage(message: Message)
fun notifyParentDataLoaded()
fun notifyParentShowActionMode(show: Boolean)
fun hideKeyboard()
fun showActionMode(show: Boolean)
fun showRecyclerBottomPadding(show: Boolean)
}

View File

@ -89,6 +89,11 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
if (::presenter.isInitialized) presenter.onViewReselected()
}
override fun onFragmentChanged() {
(parentFragmentManager.fragments.find { it is MessageFragment } as MessageFragment?)
?.onFragmentChanged()
}
override fun updateData(data: List<Pair<String, Drawable?>>) {
with(moreAdapter) {
items = data

View File

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M14,7L9,12 6,9"
android:strokeWidth="2"
android:strokeColor="#000000" />
<path
android:fillColor="#000000"
android:pathData="M16,2H4C2.895,2 2,2.895 2,4v12c0,1.105 0.895,2 2,2h12c1.105,0 2,-0.895 2,-2V4C18,2.895 17.105,2 16,2zM16,4v12H4V4H16z" />
<path
android:fillColor="#000000"
android:pathData="M6,20v2h14c1.105,0 2,-0.895 2,-2V6h-2v14H6z" />
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M16,2H4C2.895,2 2,2.895 2,4v12c0,1.105 0.895,2 2,2h12c1.105,0 2,-0.895 2,-2V4C18,2.895 17.105,2 16,2zM16,4v12H4V4H16z" />
<path
android:fillColor="#000000"
android:pathData="M6,20v2h14c1.105,0 2,-0.895 2,-2V6h-2v14H6z" />
</vector>

View File

@ -9,8 +9,10 @@
android:id="@+id/messagePreviewRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:itemCount="1"
tools:listitem="@layout/item_message_preview" />
tools:listitem="@layout/item_message_preview"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/messagePreviewError"

View File

@ -5,22 +5,34 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:paddingLeft="16dp"
android:paddingTop="10dp"
android:paddingRight="16dp"
android:paddingEnd="16dp"
android:paddingBottom="10dp"
tools:context=".ui.modules.message.tab.MessageTabAdapter">
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/messageItemCheckbox"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="16dp"
android:clickable="false"
android:focusable="false"
android:text="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/messageItemAuthor"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="10dp"
android:ellipsize="end"
android:singleLine="true"
android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@+id/messageItemDate"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="@id/messageItemCheckbox"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem/random" />
@ -45,6 +57,7 @@
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toStartOf="@id/messageItemAttachmentIcon"
app:layout_constraintStart_toEndOf="@id/messageItemCheckbox"
app:layout_constraintStart_toStartOf="@id/messageItemAuthor"
app:layout_constraintTop_toBottomOf="@+id/messageItemAuthor"
app:layout_goneMarginEnd="0dp"

View File

@ -19,7 +19,7 @@
android:id="@+id/messagePreviewMenuDelete"
android:icon="@drawable/ic_menu_message_delete"
android:orderInCategory="1"
android:title="@string/message_delete"
android:title="@string/message_move_to_trash"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/messageTabContextMenuDelete"
android:icon="@drawable/ic_menu_message_delete"
android:orderInCategory="1"
android:title="@string/message_move_to_trash"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="always" />
<item
android:id="@+id/messageTabContextMenuSelectAll"
android:icon="@drawable/ic_message_select_all"
android:orderInCategory="2"
android:title="@string/message_select_all"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
</menu>

View File

@ -287,7 +287,7 @@
<string name="message_reply">Odpověď</string>
<string name="message_forward">Poslat dále</string>
<string name="message_delete">Odstranit</string>
<string name="message_move_to_bin">Přesunout do koše</string>
<string name="message_move_to_trash">Přesunout do koše</string>
<string name="message_delete_forever">Odstranit natrvalo</string>
<string name="message_delete_success">Zpráva byla úspěšně odstraněna</string>
<string name="message_share">Sdílet</string>

View File

@ -253,7 +253,7 @@
<string name="message_reply">Antwort</string>
<string name="message_forward">Weiterleiten</string>
<string name="message_delete">Löschen</string>
<string name="message_move_to_bin">In den Korb wandern</string>
<string name="message_move_to_trash">In den Korb wandern</string>
<string name="message_delete_forever">Dauerhaft löschen</string>
<string name="message_delete_success">Nachricht erfolgreich gelöscht</string>
<string name="message_share">Teilen</string>

View File

@ -287,7 +287,7 @@
<string name="message_reply">Odpowiedz</string>
<string name="message_forward">Prześlij dalej</string>
<string name="message_delete">Usuń</string>
<string name="message_move_to_bin">Przenieś do kosza</string>
<string name="message_move_to_trash">Przenieś do kosza</string>
<string name="message_delete_forever">Usuń trwale</string>
<string name="message_delete_success">Wiadomość usunięta pomyślnie</string>
<string name="message_share">Udostępnij</string>

View File

@ -287,7 +287,7 @@
<string name="message_reply">Ответ</string>
<string name="message_forward">Переслать</string>
<string name="message_delete">Удалить</string>
<string name="message_move_to_bin">Перенести в корзину</string>
<string name="message_move_to_trash">Перенести в корзину</string>
<string name="message_delete_forever">Удалить навсегда</string>
<string name="message_delete_success">Сообщение успешно удалено</string>
<string name="message_share">Поделиться</string>

View File

@ -287,7 +287,7 @@
<string name="message_reply">Odpoveď</string>
<string name="message_forward">Poslať ďalej</string>
<string name="message_delete">Odstrániť</string>
<string name="message_move_to_bin">Presunúť do koša</string>
<string name="message_move_to_trash">Presunúť do koša</string>
<string name="message_delete_forever">Odstrániť natrvalo</string>
<string name="message_delete_success">Správa bola úspešne odstránená</string>
<string name="message_share">Zdieľať</string>

View File

@ -287,7 +287,7 @@
<string name="message_reply">Відповісти</string>
<string name="message_forward">Переслати</string>
<string name="message_delete">Видалити</string>
<string name="message_move_to_bin">Перемістити у кошик</string>
<string name="message_move_to_trash">Перемістити у кошик</string>
<string name="message_delete_forever">Видалити назавжди</string>
<string name="message_delete_success">Повідомлення було успішно видалено</string>
<string name="message_share">Поділіться</string>

View File

@ -277,11 +277,12 @@
<string name="message_no_items">No messages</string>
<string name="message_from">From:</string>
<string name="message_to">To:</string>
<string name="message_date">Date: %s</string>
<string name="message_date">Date: %1$s</string>
<string name="message_reply">Reply</string>
<string name="message_forward">Forward</string>
<string name="message_delete">Delete</string>
<string name="message_move_to_bin">Move to trash</string>
<string name="message_select_all">Select all</string>
<string name="message_unselect_all">Unselect all</string>
<string name="message_move_to_trash">Move to trash</string>
<string name="message_delete_forever">Delete permanently</string>
<string name="message_delete_success">Message deleted successfully</string>
<string name="message_share">Share</string>
@ -297,8 +298,8 @@
<string name="message_read">Read: %s</string>
<string name="message_read_by">Read by: %1$d of %2$d people</string>
<plurals name="message_number_item">
<item quantity="one">%d message</item>
<item quantity="other">%d messages</item>
<item quantity="one">%1$d message</item>
<item quantity="other">%1$d messages</item>
</plurals>
<plurals name="message_new_items">
<item quantity="one">New message</item>
@ -310,6 +311,11 @@
<item quantity="one">You received %1$d message</item>
<item quantity="other">You received %1$d messages</item>
</plurals>
<plurals name="message_selected_messages_count">
<item quantity="one">%1$d selected</item>
<item quantity="other">%1$d selected</item>
</plurals>
<string name="message_messages_deleted">Messages deleted</string>
<!--Note-->