Add draft message (#1306)

This commit is contained in:
MRmlik12 2021-07-31 18:00:22 +02:00 committed by GitHub
parent c8c9001277
commit e1c1f305c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 194 additions and 15 deletions

View File

@ -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

View File

@ -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) }
}

View File

@ -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(

View File

@ -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<RecipientChipItem>,
val subject: String,
val content: String,
)

View File

@ -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) })
}

View File

@ -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

View File

@ -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,

View File

@ -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<SendMessagePresenter, ActivitySendMessageBinding>(), SendMessageView {
class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessageBinding>(),
SendMessageView {
@Inject
override lateinit var presenter: SendMessagePresenter
@ -47,14 +52,11 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
get() = binding.sendMessageTo.isDropdownListVisible
@Suppress("UNCHECKED_CAST")
override val formRecipientsData: List<RecipientChipItem>
get() = binding.sendMessageTo.addedChipItems as List<RecipientChipItem>
override lateinit var formRecipientsData: List<RecipientChipItem>
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<SendMessagePresenter, ActivitySendMessa
override val messageSuccess: String
get() = getString(R.string.message_send_successful)
@FlowPreview
@Suppress("UNCHECKED_CAST")
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivitySendMessageBinding.inflate(layoutInflater).apply { binding = this }.root)
@ -72,7 +76,15 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
supportActionBar?.setDisplayHomeAsUpEnabled(true)
messageContainer = binding.sendMessageContainer
presenter.onAttachView(this, intent.getSerializableExtra(EXTRA_MESSAGE) as? Message, intent.getSerializableExtra(EXTRA_REPLY) as? Boolean)
formRecipientsData = binding.sendMessageTo.addedChipItems as List<RecipientChipItem>
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<SendMessagePresenter, ActivitySendMessa
setUpExtendedHitArea()
with(binding) {
sendMessageScroll.setOnTouchListener { _, _ -> 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<SendMessagePresenter, ActivitySendMessa
contentHitRect.top = contentHitRect.bottom
contentHitRect.bottom = containerHitRect.bottom
binding.sendMessageContent.touchDelegate = TouchDelegate(contentHitRect, binding.sendMessageMessageContent)
binding.sendMessageContent.touchDelegate =
TouchDelegate(contentHitRect, binding.sendMessageMessageContent)
}
with(binding.sendMessageMessageContent) {
@ -181,4 +211,24 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
}
}
}
override fun showMessageBackupDialog() {
AlertDialog.Builder(this)
.setTitle(R.string.message_title)
.setMessage(presenter.getMessageBackupContent(presenter.getRecipientsNames()))
.setPositiveButton(R.string.all_yes) { _, _ -> presenter.restoreMessageParts() }
.setNegativeButton(R.string.all_no) { _, _ -> presenter.clearDraft() }
.show()
}
override fun clearDraft() {
formRecipientsData = binding.sendMessageTo.addedChipItems as List<RecipientChipItem>
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)
}

View File

@ -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<SendMessageView>(errorHandler, studentRepository) {
private val messageUpdateChannel = Channel<Unit>()
@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)
}

View File

@ -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<RecipientChipItem>
var formRecipientsData: List<RecipientChipItem>
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()
}

View File

@ -29,4 +29,6 @@
<string name="pref_key_homework_fullscreen">homework_fullscreen</string>
<string name="pref_key_subjects_without_grades">subjects_without_grades</string>
<string name="pref_key_optional_arithmetic_average">optional_arithmetic_average</string>
<string name="pref_key_message_send_is_draft">message_send_is_draft</string>
<string name="pref_key_message_send_draft">message_send_recipients</string>
</resources>

View File

@ -242,6 +242,8 @@
<item quantity="one">New message</item>
<item quantity="other">New messages</item>
</plurals>
<string name="message_restore_dialog">Do you want to restore draft message?</string>
<string name="message_restore_dialog_with_recipients">Do you want to restore draft message with recipients: %s?</string>
<plurals name="message_notify_new_items">
<item quantity="one">You received %1$d message</item>
<item quantity="other">You received %1$d messages</item>

View File

@ -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)