From e1c1f305c489612685efcee5c18652f92dbca00b Mon Sep 17 00:00:00 2001 From: MRmlik12 <44818681+MRmlik12@users.noreply.github.com> Date: Sat, 31 Jul 2021 18:00:22 +0200 Subject: [PATCH] Add draft message (#1306) --- .../github/wulkanowy/data/RepositoryModule.kt | 5 ++ .../wulkanowy/data/db/SharedPrefProvider.kt | 8 ++- .../wulkanowy/data/db/entities/Recipient.kt | 2 + .../wulkanowy/data/pojos/MessageDraft.kt | 11 +++ .../data/repositories/MessageRepository.kt | 15 ++++ .../repositories/PreferencesRepository.kt | 1 + .../modules/message/send/RecipientChipItem.kt | 2 + .../message/send/SendMessageActivity.kt | 68 ++++++++++++++++--- .../message/send/SendMessagePresenter.kt | 64 +++++++++++++++++ .../modules/message/send/SendMessageView.kt | 15 ++-- app/src/main/res/values/preferences_keys.xml | 2 + app/src/main/res/values/strings.xml | 2 + .../repositories/MessageRepositoryTest.kt | 14 +++- 13 files changed, 194 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt index 68a3d103..a1c3cbbb 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -8,6 +8,7 @@ import androidx.preference.PreferenceManager import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager +import com.squareup.moshi.Moshi import com.fredporciuncula.flow.preferences.FlowSharedPreferences import dagger.Module import dagger.Provides @@ -85,6 +86,10 @@ internal class RepositoryModule { fun provideFlowSharedPref(sharedPreferences: SharedPreferences) = FlowSharedPreferences(sharedPreferences) + @Singleton + @Provides + fun provideMoshi() = Moshi.Builder().build() + @Singleton @Provides fun provideStudentDao(database: AppDatabase) = database.studentDao diff --git a/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefProvider.kt b/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefProvider.kt index 9301d5fa..0623d403 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefProvider.kt @@ -20,9 +20,15 @@ class SharedPrefProvider @Inject constructor( fun getLong(key: String, defaultValue: Long) = sharedPref.getLong(key, defaultValue) + fun getString(key: String) = sharedPref.getString(key, null) + fun getString(key: String, defaultValue: String): String = sharedPref.getString(key, defaultValue) ?: defaultValue - fun putString(key: String, value: String, sync: Boolean = false) { + fun getBoolean(key: String, defaultValue: Boolean): Boolean = sharedPref.getBoolean(key, defaultValue) + + fun putBoolean(key: String, value: Boolean, sync: Boolean = false) = sharedPref.edit(sync) { putBoolean(key, value) } + + fun putString(key: String, value: String?, sync: Boolean = false) { sharedPref.edit(sync) { putString(key, value) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt index 3021da72..60e67d32 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt @@ -3,8 +3,10 @@ package io.github.wulkanowy.data.db.entities import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import com.squareup.moshi.JsonClass import java.io.Serializable +@JsonClass(generateAdapter = true) @Entity(tableName = "Recipients") data class Recipient( diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt new file mode 100644 index 00000000..a79b70cd --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.data.pojos + +import com.squareup.moshi.JsonClass +import io.github.wulkanowy.ui.modules.message.send.RecipientChipItem + +@JsonClass(generateAdapter = true) +data class MessageDraft( + val recipients: List, + val subject: String, + val content: String, +) \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index 1b718ff7..2034000e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -1,5 +1,10 @@ package io.github.wulkanowy.data.repositories +import android.content.Context +import com.squareup.moshi.Moshi +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.entities.Message @@ -10,9 +15,12 @@ import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.pojos.MessageDraft +import io.github.wulkanowy.data.pojos.MessageDraftJsonAdapter import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.sdk.pojo.SentMessage +import io.github.wulkanowy.ui.modules.message.send.RecipientChipItem import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init @@ -31,7 +39,10 @@ class MessageRepository @Inject constructor( private val messagesDb: MessagesDao, private val messageAttachmentDao: MessageAttachmentDao, private val sdk: Sdk, + @ApplicationContext private val context: Context, private val refreshHelper: AutoRefreshHelper, + private val sharedPrefProvider: SharedPrefProvider, + private val moshi: Moshi, ) { private val saveFetchResultMutex = Mutex() @@ -103,4 +114,8 @@ class MessageRepository @Inject constructor( })) } else messagesDb.deleteAll(listOf(message)) } + + var draftMessage: MessageDraft? + get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft))?.let { MessageDraftJsonAdapter(moshi).fromJson(it) } + set(value) = sharedPrefProvider.putString(context.getString(R.string.pref_key_message_send_draft), value?.let { MessageDraftJsonAdapter(moshi).toJson(it) }) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 827f5e09..5b97b65d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.data.repositories import android.content.Context import android.content.SharedPreferences +import com.squareup.moshi.Moshi import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.Preference import dagger.hilt.android.qualifiers.ApplicationContext diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt index dd2d2b9b..26ab7f48 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt @@ -1,8 +1,10 @@ package io.github.wulkanowy.ui.modules.message.send +import com.squareup.moshi.JsonClass import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.materialchipsinput.ChipItem +@JsonClass(generateAdapter = true) data class RecipientChipItem( override val title: String, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt index 59944d41..f169de9f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.message.send import android.annotation.SuppressLint +import android.app.AlertDialog import android.content.Context import android.content.Intent import android.graphics.Rect @@ -12,6 +13,7 @@ import android.view.View.GONE import android.view.View.VISIBLE import android.widget.Toast import android.widget.Toast.LENGTH_LONG +import androidx.core.widget.doOnTextChanged import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Message @@ -21,10 +23,13 @@ import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.hideSoftInput import io.github.wulkanowy.utils.showSoftInput +import kotlinx.coroutines.FlowPreview +import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint -class SendMessageActivity : BaseActivity(), SendMessageView { +class SendMessageActivity : BaseActivity(), + SendMessageView { @Inject override lateinit var presenter: SendMessagePresenter @@ -47,14 +52,11 @@ class SendMessageActivity : BaseActivity - get() = binding.sendMessageTo.addedChipItems as List + override lateinit var formRecipientsData: List - override val formSubjectValue: String - get() = binding.sendMessageSubject.text.toString() + override lateinit var formSubjectValue: String - override val formContentValue: String - get() = binding.sendMessageMessageContent.text.toString() + override lateinit var formContentValue: String override val messageRequiredRecipients: String get() = getString(R.string.message_required_recipients) @@ -65,6 +67,8 @@ class SendMessageActivity : BaseActivity + formSubjectValue = binding.sendMessageSubject.text.toString() + formContentValue = binding.sendMessageMessageContent.text.toString() + + presenter.onAttachView( + view = this, + message = intent.getSerializableExtra(EXTRA_MESSAGE) as? Message, + reply = intent.getSerializableExtra(EXTRA_REPLY) as? Boolean + ) } @SuppressLint("ClickableViewAccessibility") @@ -80,10 +92,27 @@ class SendMessageActivity : BaseActivity presenter.onTouchScroll() } + sendMessageTo.onChipAddListener = { onRecipientChange() } sendMessageTo.onTextChangeListener = presenter::onRecipientsTextChange + sendMessageSubject.doOnTextChanged { text, _, _, _ -> onMessageSubjectChange(text) } + sendMessageMessageContent.doOnTextChanged { text, _, _, _ -> onMessageContentChange(text) } } } + private fun onMessageSubjectChange(text: CharSequence?) { + formSubjectValue = text.toString() + presenter.onMessageContentChange() + } + + private fun onMessageContentChange(text: CharSequence?) { + formContentValue = text.toString() + presenter.onMessageContentChange() + } + + private fun onRecipientChange() { + presenter.onMessageContentChange() + } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.action_menu_send_message, menu) return true @@ -171,7 +200,8 @@ class SendMessageActivity : BaseActivity presenter.restoreMessageParts() } + .setNegativeButton(R.string.all_no) { _, _ -> presenter.clearDraft() } + .show() + } + + override fun clearDraft() { + formRecipientsData = binding.sendMessageTo.addedChipItems as List + presenter.clearDraft() + } + + override fun getMessageBackupDialogString() = + resources.getString(R.string.message_restore_dialog) + + override fun getMessageBackupDialogStringWithRecipients(recipients: String) = + resources.getString(R.string.message_restore_dialog_with_recipients, recipients) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt index 77bd0f5e..b1cf3b64 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt @@ -1,8 +1,10 @@ package io.github.wulkanowy.ui.modules.message.send +import io.github.wulkanowy.R import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Recipient +import io.github.wulkanowy.data.pojos.MessageDraft import io.github.wulkanowy.data.repositories.MessageRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.RecipientRepository @@ -15,7 +17,14 @@ import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.toFormattedString +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -30,12 +39,19 @@ class SendMessagePresenter @Inject constructor( private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { + private val messageUpdateChannel = Channel() + + @FlowPreview fun onAttachView(view: SendMessageView, message: Message?, reply: Boolean?) { super.onAttachView(view) view.initView() + initializeSubjectStream() Timber.i("Send message view was initialized") loadData(message, reply) with(view) { + if (messageRepository.draftMessage != null && reply == null) { + view.showMessageBackupDialog() + } message?.let { setSubject(when (reply) { true -> "RE: " @@ -159,6 +175,7 @@ class SendMessagePresenter @Inject constructor( } Status.SUCCESS -> { Timber.i("Sending message result: Success") + view?.clearDraft() view?.run { showMessage(messageSuccess) popView() @@ -203,4 +220,51 @@ class SendMessagePresenter @Inject constructor( ) } } + + fun onMessageContentChange() { + launch { + messageUpdateChannel.send(Unit) + } + } + + @OptIn(FlowPreview::class) + private fun initializeSubjectStream() { + launch { + messageUpdateChannel.consumeAsFlow() + .debounce(250) + .catch { Timber.e(it) } + .collect { + saveDraftMessage() + Timber.i("Draft message was saved!") + } + } + } + + private fun saveDraftMessage() { + messageRepository.draftMessage = MessageDraft( + view?.formRecipientsData!!, + view?.formSubjectValue!!, + view?.formContentValue!! + ) + } + + fun restoreMessageParts() { + val draftMessage = messageRepository.draftMessage ?: return + view?.setSelectedRecipients(draftMessage.recipients) + view?.setSubject(draftMessage.subject) + view?.setContent(draftMessage.content) + Timber.i("Continue work on draft") + } + + fun getRecipientsNames(): String { + return messageRepository.draftMessage?.recipients.orEmpty().joinToString { it.recipient.name } + } + + fun clearDraft() { + messageRepository.draftMessage = null + Timber.i("Draft cleared!") + } + + fun getMessageBackupContent(recipients: String) = if (recipients.isEmpty()) view?.getMessageBackupDialogString() + else view?.getMessageBackupDialogStringWithRecipients(recipients) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageView.kt index 2839f9ce..21b42e3e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageView.kt @@ -4,14 +4,13 @@ import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.ui.base.BaseView interface SendMessageView : BaseView { - val isDropdownListVisible: Boolean - val formRecipientsData: List + var formRecipientsData: List - val formSubjectValue: String + var formSubjectValue: String - val formContentValue: String + var formContentValue: String val messageRequiredRecipients: String @@ -46,4 +45,12 @@ interface SendMessageView : BaseView { fun scrollToRecipients() fun popView() + + fun showMessageBackupDialog() + + fun getMessageBackupDialogString(): String + + fun getMessageBackupDialogStringWithRecipients(recipients: String): String + + fun clearDraft() } diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 5b10d5ed..876e4333 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -29,4 +29,6 @@ homework_fullscreen subjects_without_grades optional_arithmetic_average + message_send_is_draft + message_send_recipients diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a006fde..da47af3c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -242,6 +242,8 @@ New message New messages + Do you want to restore draft message? + Do you want to restore draft message with recipients: %s? You received %1$d message You received %1$d messages diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt index 2fc899b3..8b72479f 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt @@ -1,6 +1,9 @@ package io.github.wulkanowy.data.repositories +import android.content.Context +import com.squareup.moshi.Moshi import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.entities.Message @@ -38,19 +41,28 @@ class MessageRepositoryTest { @MockK private lateinit var messageAttachmentDao: MessageAttachmentDao + @MockK + private lateinit var context: Context + @MockK(relaxUnitFun = true) private lateinit var refreshHelper: AutoRefreshHelper + @MockK + private lateinit var sharedPrefProvider: SharedPrefProvider + private val student = getStudentEntity() private lateinit var messageRepository: MessageRepository + @MockK + private lateinit var moshi: Moshi + @Before fun setUp() { MockKAnnotations.init(this) every { refreshHelper.isShouldBeRefreshed(any()) } returns false - messageRepository = MessageRepository(messageDb, messageAttachmentDao, sdk, refreshHelper) + messageRepository = MessageRepository(messageDb, messageAttachmentDao, sdk, context, refreshHelper, sharedPrefProvider, moshi) } @Test(expected = NoSuchElementException::class)