1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-20 06:59:10 -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, message: Message,
markAsRead: Boolean = false, markAsRead: Boolean = false,
): Flow<Resource<MessageWithAttachment?>> = networkBoundResource( ): Flow<Resource<MessageWithAttachment?>> = networkBoundResource(
isResultEmpty = { it == null }, isResultEmpty = { it?.message?.content.isNullOrBlank() },
shouldFetch = { shouldFetch = {
checkNotNull(it) { "This message no longer exist!" } checkNotNull(it) { "This message no longer exist!" }
Timber.d("Message content in db empty: ${it.message.content.isEmpty()}") Timber.d("Message content in db empty: ${it.message.content.isEmpty()}")
@ -151,20 +151,27 @@ class MessageRepository @Inject constructor(
recipients = recipients.mapFromEntities() recipients = recipients.mapFromEntities()
) )
suspend fun deleteMessage(student: Student, message: Message) { suspend fun deleteMessages(student: Student, messages: List<Message>) {
val isDeleted = sdk.init(student).deleteMessages( val folderId = messages.first().folderId
messages = listOf(message.messageId), message.folderId val isDeleted = sdk.init(student)
) .deleteMessages(messages = messages.map { it.messageId }, folderId = folderId)
if (message.folderId != MessageFolder.TRASHED.id && isDeleted) { if (folderId != MessageFolder.TRASHED.id && isDeleted) {
val deletedMessage = message.copy(folderId = MessageFolder.TRASHED.id).apply { val deletedMessages = messages.map {
id = message.id it.copy(folderId = MessageFolder.TRASHED.id)
content = message.content .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? var draftMessage: MessageDraft?
get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft)) get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft))
?.let { json.decodeFromString(it) } ?.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.content.DialogInterface.BUTTON_POSITIVE
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.*
import android.view.Menu import android.view.View.*
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 androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -68,7 +62,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
private val actionModeCallback = object : ActionMode.Callback { private val actionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val inflater = mode.menuInflater val inflater = mode.menuInflater
inflater.inflate(R.menu.context_menu_excuse, menu) inflater.inflate(R.menu.context_menu_attendance, menu)
return true return true
} }

View File

@ -4,12 +4,14 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
import android.view.View.VISIBLE 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 com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.enums.MessageFolder.*
import io.github.wulkanowy.data.enums.MessageFolder.SENT
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
import io.github.wulkanowy.databinding.FragmentMessageBinding import io.github.wulkanowy.databinding.FragmentMessageBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter 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.messageTabLayout.elevation = requireContext().dpToPx(4f)
binding.openSendMessageButton.setOnClickListener { presenter.onSendMessageButtonClicked() } 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 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() { fun onChildFragmentLoaded() {
presenter.onChildViewLoaded() presenter.onChildViewLoaded()
} }
override fun notifyChildMessageDeleted(tabId: Int) { fun onChildFragmentShowNewMessage(show: Boolean) {
(pagerAdapter.getFragmentInstance(tabId) as? MessageTabFragment)?.onParentDeleteMessage() presenter.onChildViewShowNewMessage(show)
}
fun onFragmentChanged() {
presenter.onFragmentChanged()
} }
override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) { override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) {
@ -106,6 +132,13 @@ class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_m
?.onParentLoadData(forceRefresh) ?.onParentLoadData(forceRefresh)
} }
override fun notifyChildrenFinishActionMode() {
repeat(3) {
(pagerAdapter.getFragmentInstance(it) as? MessageTabFragment)
?.onParentFinishActionMode()
}
}
override fun openSendMessage() { override fun openSendMessage() {
context?.let { it.startActivity(SendMessageActivity.getStartIntent(it)) } 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.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -24,6 +23,7 @@ class MessagePresenter @Inject constructor(
fun onPageSelected(index: Int) { fun onPageSelected(index: Int) {
loadChild(index) loadChild(index)
view?.notifyChildrenFinishActionMode()
} }
private fun loadData() { private fun loadData() {
@ -35,6 +35,10 @@ class MessagePresenter @Inject constructor(
view?.notifyChildLoadData(index, forceRefresh) view?.notifyChildLoadData(index, forceRefresh)
} }
fun onFragmentChanged() {
view?.notifyChildrenFinishActionMode()
}
fun onChildViewLoaded() { fun onChildViewLoaded() {
view?.apply { view?.apply {
showContent(true) 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() { fun onSendMessageButtonClicked() {
view?.openSendMessage() view?.openSendMessage()
} }

View File

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

View File

@ -142,7 +142,7 @@ class MessagePreviewFragment :
} }
override fun setNotDeletedOptionsLabels() { override fun setNotDeletedOptionsLabels() {
menuDeleteButton?.setTitle(R.string.message_move_to_bin) menuDeleteButton?.setTitle(R.string.message_move_to_trash)
} }
override fun showErrorView(show: Boolean) { 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.graphics.Typeface
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.CompoundButton import android.widget.CompoundButton
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.databinding.ItemMessageBinding import io.github.wulkanowy.databinding.ItemMessageBinding
import io.github.wulkanowy.databinding.ItemMessageChipsBinding import io.github.wulkanowy.databinding.ItemMessageChipsBinding
@ -20,118 +17,141 @@ import javax.inject.Inject
class MessageTabAdapter @Inject constructor() : class MessageTabAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() { RecyclerView.Adapter<RecyclerView.ViewHolder>() {
enum class ViewType { HEADER, ITEM } var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit = { _, _ -> }
var onItemClickListener: (Message, position: Int) -> Unit = { _, _ -> } var onLongItemClickListener: (MessageTabDataItem.MessageItem) -> Unit = {}
var onHeaderClickListener: (chip: CompoundButton, isChecked: Boolean) -> Unit = { _, _ -> }
var onHeaderClickListener: (CompoundButton, Boolean) -> Unit = { _, _ -> }
var onChangesDetectedListener = {} var onChangesDetectedListener = {}
private var items = mutableListOf<MessageTabDataItem>() private var items = mutableListOf<MessageTabDataItem>()
private var onlyUnread: Boolean? = null
private var onlyWithAttachments = false
fun setDataItems( fun submitData(data: List<MessageTabDataItem>) {
data: List<MessageTabDataItem>, val originalMessagesSize = items.count { it.viewType == MessageItemViewType.MESSAGE }
onlyUnread: Boolean?, val newMessagesSize = data.count { it.viewType == MessageItemViewType.MESSAGE }
onlyWithAttachments: Boolean
) { if (originalMessagesSize != newMessagesSize) onChangesDetectedListener()
if (items.size != data.size) onChangesDetectedListener()
val diffResult = DiffUtil.calculateDiff(MessageTabDiffUtil(items, data)) val diffResult = DiffUtil.calculateDiff(MessageTabDiffUtil(items, data))
items = data.toMutableList() items = data.toMutableList()
this.onlyUnread = onlyUnread
this.onlyWithAttachments = onlyWithAttachments
diffResult.dispatchUpdatesTo(this) diffResult.dispatchUpdatesTo(this)
} }
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int) = items[position].viewType.ordinal
return when (position) {
0 -> ViewType.HEADER.ordinal
else -> ViewType.ITEM.ordinal
}
}
override fun getItemCount() = items.size override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context) 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) ItemMessageBinding.inflate(inflater, parent, false)
) )
ViewType.HEADER.ordinal -> HeaderViewHolder( MessageItemViewType.FILTERS -> HeaderViewHolder(
ItemMessageChipsBinding.inflate(inflater, parent, false) ItemMessageChipsBinding.inflate(inflater, parent, false)
) )
else -> throw IllegalStateException()
} }
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) { when (holder) {
is ItemViewHolder -> { is ItemViewHolder -> bindItemViewHolder(holder, position)
val item = (items[position] as MessageTabDataItem.MessageItem).message is HeaderViewHolder -> bindHeaderViewHolder(holder, position)
}
}
with(holder.binding) { private fun bindHeaderViewHolder(holder: HeaderViewHolder, position: Int) {
val style = if (item.unread) Typeface.BOLD else Typeface.NORMAL val item = items[position] as MessageTabDataItem.FilterHeader
messageItemAuthor.run { with(holder.binding) {
text = if (item.onlyUnread == null) {
if (item.folderId == MessageFolder.SENT.id) item.recipient else item.sender chipUnread.isVisible = false
setTypeface(null, style) } else {
} chipUnread.isVisible = true
messageItemSubject.run { chipUnread.isChecked = item.onlyUnread
text = chipUnread.setOnCheckedChangeListener(onHeaderClickListener)
if (item.subject.isNotBlank()) item.subject else context.getString(R.string.message_no_subject) }
setTypeface(null, style) chipUnread.isEnabled = item.isEnabled
} chipAttachments.isEnabled = item.isEnabled
messageItemDate.run { chipAttachments.isChecked = item.onlyWithAttachments
text = item.date.toFormattedString() chipAttachments.setOnCheckedChangeListener(onHeaderClickListener)
setTypeface(null, style) }
} }
messageItemAttachmentIcon.visibility =
if (item.hasAttachments) View.VISIBLE else View.GONE
root.setOnClickListener { private fun bindItemViewHolder(holder: ItemViewHolder, position: Int) {
holder.bindingAdapterPosition.let { val item = (items[position] as MessageTabDataItem.MessageItem)
if (it != NO_POSITION) onItemClickListener(item, it) val message = item.message
}
with(holder.binding) {
val style = if (message.unread) Typeface.BOLD else Typeface.NORMAL
messageItemAuthor.run {
text = if (message.folderId == MessageFolder.SENT.id) {
message.recipient
} else {
message.sender
}
setTypeface(null, style)
}
messageItemSubject.run {
text = message.subject.ifBlank { context.getString(R.string.message_no_subject) }
setTypeface(null, style)
}
messageItemDate.run {
text = message.date.toFormattedString()
setTypeface(null, style)
}
messageItemAttachmentIcon.isVisible = message.hasAttachments
root.setOnClickListener {
holder.bindingAdapterPosition.let {
if (it != RecyclerView.NO_POSITION) {
onItemClickListener(item, it)
} }
} }
} }
is HeaderViewHolder -> {
with(holder.binding) { root.setOnLongClickListener {
if (onlyUnread == null) chipUnread.isVisible = false onLongItemClickListener(item)
else { return@setOnLongClickListener true
chipUnread.isVisible = true }
chipUnread.isChecked = onlyUnread!!
chipUnread.setOnCheckedChangeListener(onHeaderClickListener) with(messageItemCheckbox) {
} isChecked = item.isSelected
chipAttachments.isChecked = onlyWithAttachments isVisible = item.isActionMode
chipAttachments.setOnCheckedChangeListener(onHeaderClickListener)
}
} }
} }
} }
class ItemViewHolder(val binding: ItemMessageBinding) : RecyclerView.ViewHolder(binding.root) class ItemViewHolder(val binding: ItemMessageBinding) : RecyclerView.ViewHolder(binding.root)
class HeaderViewHolder(val binding: ItemMessageChipsBinding) : class HeaderViewHolder(val binding: ItemMessageChipsBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
private class MessageTabDiffUtil( private class MessageTabDiffUtil(
private val old: List<MessageTabDataItem>, private val old: List<MessageTabDataItem>,
private val new: List<MessageTabDataItem> private val new: List<MessageTabDataItem>
) : ) : DiffUtil.Callback() {
DiffUtil.Callback() {
override fun getOldListSize(): Int = old.size override fun getOldListSize(): Int = old.size
override fun getNewListSize(): Int = new.size override fun getNewListSize(): Int = new.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 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 { override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
return old[oldItemPosition] == new[newItemPosition] 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 import io.github.wulkanowy.data.db.entities.Message
sealed class MessageTabDataItem { sealed class MessageTabDataItem(val viewType: MessageItemViewType) {
data class MessageItem(val message: Message) : MessageTabDataItem() {
override val id = message.id
}
object Header : MessageTabDataItem() { data class MessageItem(
override val id = Long.MIN_VALUE val message: Message,
} val isSelected: Boolean,
val isActionMode: Boolean
) : MessageTabDataItem(MessageItemViewType.MESSAGE)
abstract val id: Long data class FilterHeader(
val onlyUnread: Boolean?,
val onlyWithAttachments: Boolean,
val isEnabled: Boolean
) : MessageTabDataItem(MessageItemViewType.FILTERS)
} }
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.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.*
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.widget.CompoundButton import android.widget.CompoundButton
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R 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.MessageFragment
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.hideSoftInput
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -31,9 +34,10 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
lateinit var presenter: MessageTabPresenter lateinit var presenter: MessageTabPresenter
@Inject @Inject
lateinit var tabAdapter: MessageTabAdapter lateinit var messageTabAdapter: MessageTabAdapter
companion object { companion object {
const val MESSAGE_TAB_FOLDER_ID = "message_tab_folder_id" const val MESSAGE_TAB_FOLDER_ID = "message_tab_folder_id"
fun newInstance(folder: MessageFolder): MessageTabFragment { fun newInstance(folder: MessageFolder): MessageTabFragment {
@ -46,11 +50,38 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
} }
override val isViewEmpty 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -69,24 +100,25 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
} }
override fun initView() { override fun initView() {
with(tabAdapter) { with(messageTabAdapter) {
onItemClickListener = presenter::onMessageItemSelected onItemClickListener = presenter::onMessageItemSelected
onLongItemClickListener = presenter::onMessageItemLongSelected
onHeaderClickListener = ::onChipChecked onHeaderClickListener = ::onChipChecked
onChangesDetectedListener = ::resetListPosition onChangesDetectedListener = ::resetListPosition
} }
with(binding.messageTabRecycler) { with(binding.messageTabRecycler) {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = tabAdapter adapter = messageTabAdapter
addItemDecoration(DividerItemDecoration(context, false)) addItemDecoration(DividerItemDecoration(context, false))
itemAnimator = null
} }
with(binding) { with(binding) {
messageTabSwipe.setOnRefreshListener(presenter::onSwipeRefresh) messageTabSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
messageTabSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) messageTabSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
messageTabSwipe.setProgressBackgroundColorSchemeColor( messageTabSwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor( requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)
R.attr.colorSwipeRefresh
)
) )
messageTabErrorRetry.setOnClickListener { presenter.onRetry() } messageTabErrorRetry.setOnClickListener { presenter.onRetry() }
messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() } messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() }
@ -109,9 +141,28 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
}) })
} }
override fun updateData(data: List<MessageTabDataItem>, hide: Boolean) { override fun updateData(data: List<MessageTabDataItem>) {
if (hide) onlyUnread = null messageTabAdapter.submitData(data)
tabAdapter.setDataItems(data, onlyUnread, onlyWithAttachments) }
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) { override fun showProgress(show: Boolean) {
@ -146,6 +197,14 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
binding.messageTabSwipe.isRefreshing = show 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) { override fun openMessage(message: Message) {
(activity as? MainActivity)?.pushView(MessagePreviewFragment.newInstance(message)) (activity as? MainActivity)?.pushView(MessagePreviewFragment.newInstance(message))
} }
@ -154,12 +213,16 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
(parentFragment as? MessageFragment)?.onChildFragmentLoaded() (parentFragment as? MessageFragment)?.onChildFragmentLoaded()
} }
fun onParentLoadData( override fun notifyParentShowActionMode(show: Boolean) {
forceRefresh: Boolean, (parentFragment as? MessageFragment)?.onChildFragmentShowActionMode(show)
onlyUnread: Boolean? = this.onlyUnread, }
onlyWithAttachments: Boolean = this.onlyWithAttachments
) { fun onParentLoadData(forceRefresh: Boolean) {
presenter.onParentViewLoadData(forceRefresh, onlyUnread, onlyWithAttachments) presenter.onParentViewLoadData(forceRefresh)
}
fun onParentFinishActionMode() {
presenter.onParentFinishActionMode()
} }
private fun onChipChecked(chip: CompoundButton, isChecked: Boolean) { private fun onChipChecked(chip: CompoundButton, isChecked: Boolean) {
@ -169,8 +232,22 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
} }
} }
fun onParentDeleteMessage() { override fun showActionMode(show: Boolean) {
presenter.onDeleteMessage() 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) { override fun onSaveInstanceState(outState: Bundle) {

View File

@ -12,7 +12,10 @@ import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel 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 kotlinx.coroutines.launch
import me.xdrop.fuzzywuzzy.FuzzySearch import me.xdrop.fuzzywuzzy.FuzzySearch
import timber.log.Timber import timber.log.Timber
@ -37,6 +40,14 @@ class MessageTabPresenter @Inject constructor(
private val searchChannel = Channel<String>() 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) { fun onAttachView(view: MessageTabView, folder: MessageFolder) {
super.onAttachView(view) super.onAttachView(view)
view.initView() view.initView()
@ -47,14 +58,14 @@ class MessageTabPresenter @Inject constructor(
fun onSwipeRefresh() { fun onSwipeRefresh() {
Timber.i("Force refreshing the $folder message") Timber.i("Force refreshing the $folder message")
view?.run { onParentViewLoadData(true, onlyUnread, onlyWithAttachments) } view?.run { loadData(true) }
} }
fun onRetry() { fun onRetry() {
view?.run { view?.run {
showErrorView(false) showErrorView(false)
showProgress(true) showProgress(true)
loadData(true, onlyUnread == true, onlyWithAttachments) loadData(true)
} }
} }
@ -62,42 +73,135 @@ class MessageTabPresenter @Inject constructor(
view?.showErrorDetailsDialog(lastError) view?.showErrorDetailsDialog(lastError)
} }
fun onDeleteMessage() { fun onParentViewLoadData(forceRefresh: Boolean) {
view?.run { loadData(true, onlyUnread == true, onlyWithAttachments) } loadData(forceRefresh)
} }
fun onParentViewLoadData( fun onParentFinishActionMode() {
forceRefresh: Boolean, view?.showActionMode(false)
onlyUnread: Boolean? = view?.onlyUnread,
onlyWithAttachments: Boolean = view?.onlyWithAttachments == true
) {
loadData(forceRefresh, onlyUnread == true, onlyWithAttachments)
} }
fun onMessageItemSelected(message: Message, position: Int) { fun onDestroyActionMode() {
Timber.i("Select message ${message.id} item (position: $position)") isActionMode = false
view?.openMessage(message) 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) { fun onUnreadFilterSelected(isChecked: Boolean) {
view?.run { view?.run {
onlyUnread = isChecked onlyUnread = isChecked
onParentViewLoadData(false, onlyUnread, onlyWithAttachments) loadData(false)
} }
} }
fun onAttachmentsFilterSelected(isChecked: Boolean) { fun onAttachmentsFilterSelected(isChecked: Boolean) {
view?.run { view?.run {
onlyWithAttachments = isChecked onlyWithAttachments = isChecked
onParentViewLoadData(false, onlyUnread, onlyWithAttachments) loadData(false)
} }
} }
private fun loadData( private fun loadData(forceRefresh: Boolean) {
forceRefresh: Boolean,
onlyUnread: Boolean,
onlyWithAttachments: Boolean
) {
Timber.i("Loading $folder message data started") Timber.i("Loading $folder message data started")
flatResourceFlow { flatResourceFlow {
@ -106,54 +210,29 @@ class MessageTabPresenter @Inject constructor(
messageRepository.getMessages(student, semester, folder, forceRefresh) messageRepository.getMessages(student, semester, folder, forceRefresh)
} }
.logResourceStatus("load $folder message") .logResourceStatus("load $folder message")
.onEach { .onResourceData {
when (it) { messages = it
is Resource.Intermediate -> {
if (it.data.isNotEmpty()) {
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)
}
val messageItemsWithHeader =
listOf(MessageTabDataItem.Header) + messageItems
updateData( val filteredData = getFilteredData()
messageItemsWithHeader,
folder.id == MessageFolder.SENT.id view?.run {
) enableSwipe(true)
notifyParentDataLoaded() showErrorView(false)
} showProgress(false)
} showContent(true)
} showEmpty(filteredData.isEmpty())
is Resource.Success -> {
messages = it.data
updateData(
getFilteredData(
lastSearchQuery,
onlyUnread,
onlyWithAttachments
)
)
analytics.logEvent(
"load_data",
"type" to "messages",
"items" to it.data.size,
"folder" to folder.name
)
}
else -> {}
} }
updateDataInView()
}
.onResourceIntermediate { view?.showRefresh(true) }
.onResourceSuccess {
analytics.logEvent(
"load_data",
"type" to "messages",
"items" to it.size,
"folder" to folder.name
)
} }
.onResourceNotLoading { .onResourceNotLoading {
view?.run { view?.run {
@ -196,56 +275,71 @@ class MessageTabPresenter @Inject constructor(
.debounce(250) .debounce(250)
.map { query -> .map { query ->
lastSearchQuery = query lastSearchQuery = query
val isOnlyUnread = view?.onlyUnread == true
val isOnlyWithAttachments = view?.onlyWithAttachments == true getFilteredData()
getFilteredData(query, isOnlyUnread, isOnlyWithAttachments)
} }
.catch { Timber.e(it) } .catch { Timber.e(it) }
.collect { .collect {
Timber.d("Applying filter. Full list: ${messages.size}, filtered: ${it.size}") 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() view?.resetListPosition()
} }
} }
} }
private fun getFilteredData( private fun getFilteredData(): List<Message> {
query: String, if (lastSearchQuery.trim().isEmpty()) {
onlyUnread: Boolean = false,
onlyWithAttachments: Boolean = false
): List<Message> {
if (query.trim().isEmpty()) {
val sortedMessages = messages.sortedByDescending { it.date } val sortedMessages = messages.sortedByDescending { it.date }
return when { return when {
onlyUnread && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments } (onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments }
onlyUnread -> sortedMessages.filter { it.unread == onlyUnread } (onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread }
onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments } onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments }
else -> sortedMessages else -> sortedMessages
} }
} else { } else {
val sortedMessages = messages 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 }) .sortedWith(compareBy<Pair<Message, Int>> { -it.second }.thenByDescending { it.first.date })
.filter { it.second > 6000 } .filter { it.second > 6000 }
.map { it.first } .map { it.first }
return when { return when {
onlyUnread && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments } (onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments }
onlyUnread -> sortedMessages.filter { it.unread == onlyUnread } (onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread }
onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments } onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments }
else -> sortedMessages else -> sortedMessages
} }
} }
} }
private fun updateData(data: List<Message>) { private fun updateDataInView() {
view?.run { val data = getFilteredData()
showEmpty(data.isEmpty())
showContent(true) val list = buildList {
showErrorView(false) add(
val newItems = MessageTabDataItem.FilterHeader(
listOf(MessageTabDataItem.Header) + data.map { MessageTabDataItem.MessageItem(it) } onlyUnread = onlyUnread.takeIf { folder != MessageFolder.SENT },
updateData(newItems, folder.id == MessageFolder.SENT.id) 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 { private fun calculateMatchRatio(message: Message, query: String): Int {

View File

@ -7,15 +7,15 @@ interface MessageTabView : BaseView {
val isViewEmpty: Boolean val isViewEmpty: Boolean
var onlyUnread: Boolean?
var onlyWithAttachments: Boolean
fun initView() fun initView()
fun resetListPosition() 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) fun showProgress(show: Boolean)
@ -25,8 +25,12 @@ interface MessageTabView : BaseView {
fun showEmpty(show: Boolean) fun showEmpty(show: Boolean)
fun showMessagesDeleted()
fun showErrorView(show: Boolean) fun showErrorView(show: Boolean)
fun notifyParentShowNewMessage(show: Boolean)
fun setErrorDetails(message: String) fun setErrorDetails(message: String)
fun showRefresh(show: Boolean) fun showRefresh(show: Boolean)
@ -34,4 +38,12 @@ interface MessageTabView : BaseView {
fun openMessage(message: Message) fun openMessage(message: Message)
fun notifyParentDataLoaded() 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() 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?>>) { override fun updateData(data: List<Pair<String, Drawable?>>) {
with(moreAdapter) { with(moreAdapter) {
items = data 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:id="@+id/messagePreviewRecycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone"
tools:itemCount="1" tools:itemCount="1"
tools:listitem="@layout/item_message_preview" /> tools:listitem="@layout/item_message_preview"
tools:visibility="visible" />
<LinearLayout <LinearLayout
android:id="@+id/messagePreviewError" android:id="@+id/messagePreviewError"

View File

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

View File

@ -19,7 +19,7 @@
android:id="@+id/messagePreviewMenuDelete" android:id="@+id/messagePreviewMenuDelete"
android:icon="@drawable/ic_menu_message_delete" android:icon="@drawable/ic_menu_message_delete"
android:orderInCategory="1" android:orderInCategory="1"
android:title="@string/message_delete" android:title="@string/message_move_to_trash"
app:iconTint="@color/material_on_surface_emphasis_medium" app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item <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_reply">Odpověď</string>
<string name="message_forward">Poslat dále</string> <string name="message_forward">Poslat dále</string>
<string name="message_delete">Odstranit</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_forever">Odstranit natrvalo</string>
<string name="message_delete_success">Zpráva byla úspěšně odstraněna</string> <string name="message_delete_success">Zpráva byla úspěšně odstraněna</string>
<string name="message_share">Sdílet</string> <string name="message_share">Sdílet</string>

View File

@ -253,7 +253,7 @@
<string name="message_reply">Antwort</string> <string name="message_reply">Antwort</string>
<string name="message_forward">Weiterleiten</string> <string name="message_forward">Weiterleiten</string>
<string name="message_delete">Löschen</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_forever">Dauerhaft löschen</string>
<string name="message_delete_success">Nachricht erfolgreich gelöscht</string> <string name="message_delete_success">Nachricht erfolgreich gelöscht</string>
<string name="message_share">Teilen</string> <string name="message_share">Teilen</string>

View File

@ -287,7 +287,7 @@
<string name="message_reply">Odpowiedz</string> <string name="message_reply">Odpowiedz</string>
<string name="message_forward">Prześlij dalej</string> <string name="message_forward">Prześlij dalej</string>
<string name="message_delete">Usuń</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_forever">Usuń trwale</string>
<string name="message_delete_success">Wiadomość usunięta pomyślnie</string> <string name="message_delete_success">Wiadomość usunięta pomyślnie</string>
<string name="message_share">Udostępnij</string> <string name="message_share">Udostępnij</string>

View File

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

View File

@ -287,7 +287,7 @@
<string name="message_reply">Odpoveď</string> <string name="message_reply">Odpoveď</string>
<string name="message_forward">Poslať ďalej</string> <string name="message_forward">Poslať ďalej</string>
<string name="message_delete">Odstrániť</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_forever">Odstrániť natrvalo</string>
<string name="message_delete_success">Správa bola úspešne odstránená</string> <string name="message_delete_success">Správa bola úspešne odstránená</string>
<string name="message_share">Zdieľať</string> <string name="message_share">Zdieľať</string>

View File

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

View File

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