Merge branch 'develop' into feature/attendance-excuse-whole-day

This commit is contained in:
Mikołaj Pich 2024-03-01 21:16:06 +01:00
commit 1591d494ab
13 changed files with 192 additions and 51 deletions

View File

@ -3,5 +3,10 @@ package io.github.wulkanowy.data.enums
enum class MessageFolder(val id: Int = 1) {
RECEIVED(1),
SENT(2),
TRASHED(3)
TRASHED(3),
;
companion object {
fun byId(id: Int) = entries.first { it.id == id }
}
}

View File

@ -18,6 +18,7 @@ import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder
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.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities
@ -25,6 +26,7 @@ import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
import io.github.wulkanowy.sdk.Sdk
@ -34,7 +36,6 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@ -155,17 +156,30 @@ class MessageRepository @Inject constructor(
subject: String,
content: String,
recipients: List<Recipient>,
mailboxId: String,
mailbox: Mailbox,
) {
sdk.init(student).sendMessage(
subject = subject,
content = content,
recipients = recipients.mapFromEntities(),
mailboxId = mailboxId,
mailboxId = mailbox.globalKey,
)
refreshFolders(student, mailbox, listOf(SENT))
}
suspend fun deleteMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) {
suspend fun restoreMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) {
sdk.init(student).restoreMessages(
messages = messages.map { it.messageGlobalKey },
)
refreshFolders(student, mailbox)
}
suspend fun deleteMessage(student: Student, message: Message) {
deleteMessages(student, listOf(message))
}
suspend fun deleteMessages(student: Student, messages: List<Message>) {
val firstMessage = messages.first()
sdk.init(student).deleteMessages(
messages = messages.map { it.messageGlobalKey },
@ -184,18 +198,24 @@ class MessageRepository @Inject constructor(
}
messagesDb.updateAll(deletedMessages)
} else messagesDb.deleteAll(messages)
getMessages(
student = student,
mailbox = mailbox,
folder = TRASHED,
forceRefresh = true,
).first()
} else {
messagesDb.deleteAll(messages)
}
}
suspend fun deleteMessage(student: Student, mailbox: Mailbox?, message: Message) {
deleteMessages(student, mailbox, listOf(message))
private suspend fun refreshFolders(
student: Student,
mailbox: Mailbox?,
folders: List<MessageFolder> = MessageFolder.entries
) {
folders.forEach {
getMessages(
student = student,
mailbox = mailbox,
folder = it,
forceRefresh = true,
).toFirstResult()
}
}
suspend fun getMailboxes(student: Student, forceRefresh: Boolean) = networkBoundResource(
@ -240,7 +260,7 @@ class MessageRepository @Inject constructor(
value?.let { json.encodeToString(it) }
)
suspend fun isMuted(author: String): Boolean {
private suspend fun isMuted(author: String): Boolean {
return mutedMessageSendersDao.checkMute(author)
}

View File

@ -44,8 +44,12 @@ class MessagePreviewFragment :
private var menuForwardButton: MenuItem? = null
private var menuRestoreButton: MenuItem? = null
private var menuDeleteButton: MenuItem? = null
private var menuDeleteForeverButton: MenuItem? = null
private var menuShareButton: MenuItem? = null
private var menuPrintButton: MenuItem? = null
@ -64,6 +68,9 @@ class MessagePreviewFragment :
override val unmuteMessageSuccessString: String
get() = getString(R.string.message_unmute_success)
override val restoreMessageSuccessString: String
get() = getString(R.string.message_restore_success)
override val messageNoSubjectString: String
get() = getString(R.string.message_no_subject)
@ -111,7 +118,9 @@ class MessagePreviewFragment :
inflater.inflate(R.menu.action_menu_message_preview, menu)
menuReplyButton = menu.findItem(R.id.messagePreviewMenuReply)
menuForwardButton = menu.findItem(R.id.messagePreviewMenuForward)
menuRestoreButton = menu.findItem(R.id.messagePreviewMenuRestore)
menuDeleteButton = menu.findItem(R.id.messagePreviewMenuDelete)
menuDeleteForeverButton = menu.findItem(R.id.messagePreviewMenuDeleteForever)
menuShareButton = menu.findItem(R.id.messagePreviewMenuShare)
menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint)
menuMuteButton = menu.findItem(R.id.messagePreviewMenuMute)
@ -124,7 +133,9 @@ class MessagePreviewFragment :
return when (item.itemId) {
R.id.messagePreviewMenuReply -> presenter.onReply()
R.id.messagePreviewMenuForward -> presenter.onForward()
R.id.messagePreviewMenuRestore -> presenter.onMessageRestore()
R.id.messagePreviewMenuDelete -> presenter.onMessageDelete()
R.id.messagePreviewMenuDeleteForever -> presenter.onMessageDelete()
R.id.messagePreviewMenuShare -> presenter.onShare()
R.id.messagePreviewMenuPrint -> presenter.onPrint()
R.id.messagePreviewMenuMute -> presenter.onMute()
@ -152,23 +163,17 @@ class MessagePreviewFragment :
binding.messagePreviewRecycler.visibility = if (show) VISIBLE else GONE
}
override fun showOptions(show: Boolean, isReplayable: Boolean) {
menuReplyButton?.isVisible = isReplayable
override fun showOptions(show: Boolean, isReplayable: Boolean, isRestorable: Boolean) {
menuReplyButton?.isVisible = show && isReplayable
menuForwardButton?.isVisible = show
menuDeleteButton?.isVisible = show
menuRestoreButton?.isVisible = show && isRestorable
menuDeleteButton?.isVisible = show && !isRestorable
menuDeleteForeverButton?.isVisible = show && isRestorable
menuShareButton?.isVisible = show
menuPrintButton?.isVisible = show
menuMuteButton?.isVisible = show && isReplayable
}
override fun setDeletedOptionsLabels() {
menuDeleteButton?.setTitle(R.string.message_delete_forever)
}
override fun setNotDeletedOptionsLabels() {
menuDeleteButton?.setTitle(R.string.message_move_to_trash)
}
override fun showErrorView(show: Boolean) {
binding.messagePreviewError.visibility = if (show) VISIBLE else GONE
}

View File

@ -14,9 +14,11 @@ import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
class MessagePreviewPresenter @Inject constructor(
errorHandler: ErrorHandler,
@ -74,6 +76,7 @@ class MessagePreviewPresenter @Inject constructor(
}
}
} else {
delay(1.seconds)
view?.run {
showMessage(messageNotExists)
popView()
@ -172,13 +175,51 @@ class MessagePreviewPresenter @Inject constructor(
return true
}
private fun restoreMessage() {
val message = messageWithAttachments?.message ?: return
view?.run {
showContent(false)
showProgress(true)
showOptions(
show = false,
isReplayable = false,
isRestorable = false,
)
showErrorView(false)
}
Timber.i("Restore message ${message.messageGlobalKey}")
presenterScope.launch {
runCatching {
val student = studentRepository.getCurrentStudent(decryptPass = true)
val mailbox = messageRepository.getMailboxByStudent(student)
messageRepository.restoreMessages(student, mailbox, listOfNotNull(message))
}
.onFailure {
retryCallback = { onMessageRestore() }
errorHandler.dispatch(it)
}
.onSuccess {
view?.run {
showMessage(restoreMessageSuccessString)
popView()
}
}
view?.showProgress(false)
}
}
private fun deleteMessage() {
messageWithAttachments?.message ?: return
view?.run {
showContent(false)
showProgress(true)
showOptions(show = false, isReplayable = false)
showOptions(
show = false,
isReplayable = false,
isRestorable = false,
)
showErrorView(false)
}
@ -187,8 +228,7 @@ class MessagePreviewPresenter @Inject constructor(
presenterScope.launch {
runCatching {
val student = studentRepository.getCurrentStudent(decryptPass = true)
val mailbox = messageRepository.getMailboxByStudent(student)
messageRepository.deleteMessage(student, mailbox, messageWithAttachments?.message!!)
messageRepository.deleteMessage(student, messageWithAttachments?.message!!)
}.onFailure {
retryCallback = { onMessageDelete() }
errorHandler.dispatch(it)
@ -213,6 +253,11 @@ class MessagePreviewPresenter @Inject constructor(
}
}
fun onMessageRestore(): Boolean {
restoreMessage()
return true
}
fun onMessageDelete(): Boolean {
deleteMessage()
return true
@ -222,15 +267,9 @@ class MessagePreviewPresenter @Inject constructor(
view?.apply {
showOptions(
show = messageWithAttachments?.message != null,
isReplayable = messageWithAttachments?.message?.folderId != MessageFolder.SENT.id,
isReplayable = messageWithAttachments?.message?.folderId == MessageFolder.RECEIVED.id,
isRestorable = messageWithAttachments?.message?.folderId == MessageFolder.TRASHED.id,
)
messageWithAttachments?.message?.let {
when (it.folderId == MessageFolder.TRASHED.id) {
true -> setDeletedOptionsLabels()
false -> setNotDeletedOptionsLabels()
}
}
}
}

View File

@ -13,6 +13,8 @@ interface MessagePreviewView : BaseView {
val unmuteMessageSuccessString: String
val restoreMessageSuccessString: String
val messageNoSubjectString: String
val printHTML: String
@ -35,11 +37,7 @@ interface MessagePreviewView : BaseView {
fun setErrorRetryCallback(callback: () -> Unit)
fun showOptions(show: Boolean, isReplayable: Boolean)
fun setDeletedOptionsLabels()
fun setNotDeletedOptionsLabels()
fun showOptions(show: Boolean, isReplayable: Boolean, isRestorable: Boolean)
fun openMessageReply(message: Message?)

View File

@ -203,7 +203,7 @@ class SendMessagePresenter @Inject constructor(
subject = subject,
content = content,
recipients = recipients,
mailboxId = mailbox.globalKey,
mailbox = mailbox,
)
}.logResourceStatus("sending message").onEach {
when (it) {

View File

@ -5,7 +5,9 @@ import android.view.Menu
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.INVISIBLE
import android.view.View.VISIBLE
import android.widget.CompoundButton
import androidx.annotation.StringRes
import androidx.appcompat.view.ActionMode
@ -64,10 +66,12 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
}
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)
}
val isTrashFolder = presenter.folder == MessageFolder.TRASHED
menu.findItem(R.id.messageTabContextMenuDelete).setVisible(!isTrashFolder)
menu.findItem(R.id.messageTabContextMenuDeleteForever).setVisible(isTrashFolder)
menu.findItem(R.id.messageTabContextMenuRestore).setVisible(isTrashFolder)
return presenter.onPrepareActionMode()
}
@ -79,6 +83,8 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
override fun onActionItemClicked(mode: ActionMode, menu: MenuItem): Boolean {
when (menu.itemId) {
R.id.messageTabContextMenuDelete -> presenter.onActionModeSelectDelete()
R.id.messageTabContextMenuRestore -> presenter.onActionModeSelectRestore()
R.id.messageTabContextMenuDeleteForever -> presenter.onActionModeSelectDelete()
R.id.messageTabContextMenuSelectAll -> presenter.onActionModeSelectCheckAll()
}
return true

View File

@ -121,8 +121,27 @@ class MessageTabPresenter @Inject constructor(
return true
}
fun onActionModeSelectRestore() {
Timber.i("Restore ${messagesToDelete.size} messages")
val messageList = messagesToDelete.toList()
presenterScope.launch {
view?.run {
showProgress(true)
showContent(false)
showActionMode(false)
}
runCatching {
val student = studentRepository.getCurrentStudent(true)
messageRepository.restoreMessages(student, selectedMailbox, messageList)
}
.onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessage(R.string.message_messages_restored) }
}
}
fun onActionModeSelectDelete() {
Timber.i("Delete ${messagesToDelete.size} messages)")
Timber.i("Delete ${messagesToDelete.size} messages")
val messageList = messagesToDelete.toList()
presenterScope.launch {
@ -134,7 +153,7 @@ class MessageTabPresenter @Inject constructor(
runCatching {
val student = studentRepository.getCurrentStudent(true)
messageRepository.deleteMessages(student, selectedMailbox, messageList)
messageRepository.deleteMessages(student, messageList)
}
.onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessage(R.string.message_messages_deleted) }

View File

@ -0,0 +1,9 @@
<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="#fff"
android:pathData="M14.12,10.47L12,12.59l-2.13,-2.12 -1.41,1.41L10.59,14l-2.12,2.12 1.41,1.41L12,15.41l2.12,2.12 1.41,-1.41L13.41,14l2.12,-2.12zM15.5,4l-1,-1h-5l-1,1H5v2h14V4zM6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM8,9h8v10H8V9z" />
</vector>

View File

@ -0,0 +1,9 @@
<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="#fff"
android:pathData="M15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4zM6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8,14L8,9h8v10L8,19v-5zM10,18h4v-4h2l-4,-4 -4,4h2z" />
</vector>

View File

@ -29,6 +29,13 @@
android:title="@string/message_forward"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item
android:id="@+id/messagePreviewMenuRestore"
android:icon="@drawable/ic_menu_message_restore"
android:orderInCategory="1"
android:title="@string/message_restore_from_trash"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item
android:id="@+id/messagePreviewMenuDelete"
android:icon="@drawable/ic_menu_message_delete"
@ -36,6 +43,13 @@
android:title="@string/message_move_to_trash"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item
android:id="@+id/messagePreviewMenuDeleteForever"
android:icon="@drawable/ic_menu_message_delete_forever"
android:orderInCategory="1"
android:title="@string/message_delete_forever"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item
android:id="@+id/messagePreviewMenuMute"
android:icon="@drawable/ic_settings_notifications"

View File

@ -1,6 +1,13 @@
<?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/messageTabContextMenuRestore"
android:icon="@drawable/ic_menu_message_restore"
android:orderInCategory="1"
android:title="@string/message_restore_from_trash"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="always" />
<item
android:id="@+id/messageTabContextMenuDelete"
android:icon="@drawable/ic_menu_message_delete"
@ -8,6 +15,13 @@
android:title="@string/message_move_to_trash"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="always" />
<item
android:id="@+id/messageTabContextMenuDeleteForever"
android:icon="@drawable/ic_menu_message_delete_forever"
android:orderInCategory="1"
android:title="@string/message_delete_forever"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="always" />
<item
android:id="@+id/messageTabContextMenuSelectAll"
android:icon="@drawable/ic_message_select_all"

View File

@ -327,8 +327,10 @@
<string name="message_forward">Forward</string>
<string name="message_select_all">Select all</string>
<string name="message_unselect_all">Unselect all</string>
<string name="message_restore_from_trash">Restore from trash</string>
<string name="message_move_to_trash">Move to trash</string>
<string name="message_delete_forever">Delete permanently</string>
<string name="message_restore_success">Message restored successfully</string>
<string name="message_delete_success">Message deleted successfully</string>
<string name="message_mailbox_type_student">student</string>
<string name="message_mailbox_type_parent">parent</string>
@ -366,6 +368,7 @@
<item quantity="other">%1$d selected</item>
</plurals>
<string name="message_messages_deleted">Messages deleted</string>
<string name="message_messages_restored">Messages restored</string>
<string name="message_mailbox_chooser_title">Choose mailbox</string>
<string name="message_incognito_mode_on">Incognito mode is on</string>
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string>