1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2025-01-31 12:58:21 +01:00

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.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.chuckerteam.chucker.api.RetentionManager import com.chuckerteam.chucker.api.RetentionManager
import com.squareup.moshi.Moshi
import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -85,6 +86,10 @@ internal class RepositoryModule {
fun provideFlowSharedPref(sharedPreferences: SharedPreferences) = fun provideFlowSharedPref(sharedPreferences: SharedPreferences) =
FlowSharedPreferences(sharedPreferences) FlowSharedPreferences(sharedPreferences)
@Singleton
@Provides
fun provideMoshi() = Moshi.Builder().build()
@Singleton @Singleton
@Provides @Provides
fun provideStudentDao(database: AppDatabase) = database.studentDao 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 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 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) } 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.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.squareup.moshi.JsonClass
import java.io.Serializable import java.io.Serializable
@JsonClass(generateAdapter = true)
@Entity(tableName = "Recipients") @Entity(tableName = "Recipients")
data class Recipient( 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 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.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.entities.Message 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.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities 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.Sdk
import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.sdk.pojo.Folder
import io.github.wulkanowy.sdk.pojo.SentMessage 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.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.init
@ -31,7 +39,10 @@ class MessageRepository @Inject constructor(
private val messagesDb: MessagesDao, private val messagesDb: MessagesDao,
private val messageAttachmentDao: MessageAttachmentDao, private val messageAttachmentDao: MessageAttachmentDao,
private val sdk: Sdk, private val sdk: Sdk,
@ApplicationContext private val context: Context,
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
private val sharedPrefProvider: SharedPrefProvider,
private val moshi: Moshi,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
@ -103,4 +114,8 @@ class MessageRepository @Inject constructor(
})) }))
} else messagesDb.deleteAll(listOf(message)) } 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.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import com.squareup.moshi.Moshi
import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.fredporciuncula.flow.preferences.Preference import com.fredporciuncula.flow.preferences.Preference
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext

View File

@ -1,8 +1,10 @@
package io.github.wulkanowy.ui.modules.message.send 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.data.db.entities.Recipient
import io.github.wulkanowy.materialchipsinput.ChipItem import io.github.wulkanowy.materialchipsinput.ChipItem
@JsonClass(generateAdapter = true)
data class RecipientChipItem( data class RecipientChipItem(
override val title: String, override val title: String,

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.message.send package io.github.wulkanowy.ui.modules.message.send
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
@ -12,6 +13,7 @@ import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.widget.Toast import android.widget.Toast
import android.widget.Toast.LENGTH_LONG import android.widget.Toast.LENGTH_LONG
import androidx.core.widget.doOnTextChanged
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.db.entities.Message 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.dpToPx
import io.github.wulkanowy.utils.hideSoftInput import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.showSoftInput import io.github.wulkanowy.utils.showSoftInput
import kotlinx.coroutines.FlowPreview
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessageBinding>(), SendMessageView { class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessageBinding>(),
SendMessageView {
@Inject @Inject
override lateinit var presenter: SendMessagePresenter override lateinit var presenter: SendMessagePresenter
@ -47,14 +52,11 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
get() = binding.sendMessageTo.isDropdownListVisible get() = binding.sendMessageTo.isDropdownListVisible
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override val formRecipientsData: List<RecipientChipItem> override lateinit var formRecipientsData: List<RecipientChipItem>
get() = binding.sendMessageTo.addedChipItems as List<RecipientChipItem>
override val formSubjectValue: String override lateinit var formSubjectValue: String
get() = binding.sendMessageSubject.text.toString()
override val formContentValue: String override lateinit var formContentValue: String
get() = binding.sendMessageMessageContent.text.toString()
override val messageRequiredRecipients: String override val messageRequiredRecipients: String
get() = getString(R.string.message_required_recipients) get() = getString(R.string.message_required_recipients)
@ -65,6 +67,8 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
override val messageSuccess: String override val messageSuccess: String
get() = getString(R.string.message_send_successful) get() = getString(R.string.message_send_successful)
@FlowPreview
@Suppress("UNCHECKED_CAST")
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivitySendMessageBinding.inflate(layoutInflater).apply { binding = this }.root) setContentView(ActivitySendMessageBinding.inflate(layoutInflater).apply { binding = this }.root)
@ -72,7 +76,15 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
messageContainer = binding.sendMessageContainer 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") @SuppressLint("ClickableViewAccessibility")
@ -80,10 +92,27 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
setUpExtendedHitArea() setUpExtendedHitArea()
with(binding) { with(binding) {
sendMessageScroll.setOnTouchListener { _, _ -> presenter.onTouchScroll() } sendMessageScroll.setOnTouchListener { _, _ -> presenter.onTouchScroll() }
sendMessageTo.onChipAddListener = { onRecipientChange() }
sendMessageTo.onTextChangeListener = presenter::onRecipientsTextChange 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 { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.action_menu_send_message, menu) menuInflater.inflate(R.menu.action_menu_send_message, menu)
return true return true
@ -171,7 +200,8 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
contentHitRect.top = contentHitRect.bottom contentHitRect.top = contentHitRect.bottom
contentHitRect.bottom = containerHitRect.bottom contentHitRect.bottom = containerHitRect.bottom
binding.sendMessageContent.touchDelegate = TouchDelegate(contentHitRect, binding.sendMessageMessageContent) binding.sendMessageContent.touchDelegate =
TouchDelegate(contentHitRect, binding.sendMessageMessageContent)
} }
with(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 package io.github.wulkanowy.ui.modules.message.send
import io.github.wulkanowy.R
import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Recipient 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.MessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.RecipientRepository 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.afterLoading
import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResource
import io.github.wulkanowy.utils.toFormattedString 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.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -30,12 +39,19 @@ class SendMessagePresenter @Inject constructor(
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper
) : BasePresenter<SendMessageView>(errorHandler, studentRepository) { ) : BasePresenter<SendMessageView>(errorHandler, studentRepository) {
private val messageUpdateChannel = Channel<Unit>()
@FlowPreview
fun onAttachView(view: SendMessageView, message: Message?, reply: Boolean?) { fun onAttachView(view: SendMessageView, message: Message?, reply: Boolean?) {
super.onAttachView(view) super.onAttachView(view)
view.initView() view.initView()
initializeSubjectStream()
Timber.i("Send message view was initialized") Timber.i("Send message view was initialized")
loadData(message, reply) loadData(message, reply)
with(view) { with(view) {
if (messageRepository.draftMessage != null && reply == null) {
view.showMessageBackupDialog()
}
message?.let { message?.let {
setSubject(when (reply) { setSubject(when (reply) {
true -> "RE: " true -> "RE: "
@ -159,6 +175,7 @@ class SendMessagePresenter @Inject constructor(
} }
Status.SUCCESS -> { Status.SUCCESS -> {
Timber.i("Sending message result: Success") Timber.i("Sending message result: Success")
view?.clearDraft()
view?.run { view?.run {
showMessage(messageSuccess) showMessage(messageSuccess)
popView() 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 import io.github.wulkanowy.ui.base.BaseView
interface SendMessageView : BaseView { interface SendMessageView : BaseView {
val isDropdownListVisible: Boolean 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 val messageRequiredRecipients: String
@ -46,4 +45,12 @@ interface SendMessageView : BaseView {
fun scrollToRecipients() fun scrollToRecipients()
fun popView() 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_homework_fullscreen">homework_fullscreen</string>
<string name="pref_key_subjects_without_grades">subjects_without_grades</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_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> </resources>

View File

@ -242,6 +242,8 @@
<item quantity="one">New message</item> <item quantity="one">New message</item>
<item quantity="other">New messages</item> <item quantity="other">New messages</item>
</plurals> </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"> <plurals name="message_notify_new_items">
<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>

View File

@ -1,6 +1,9 @@
package io.github.wulkanowy.data.repositories 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.Status
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
@ -38,19 +41,28 @@ class MessageRepositoryTest {
@MockK @MockK
private lateinit var messageAttachmentDao: MessageAttachmentDao private lateinit var messageAttachmentDao: MessageAttachmentDao
@MockK
private lateinit var context: Context
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
private lateinit var refreshHelper: AutoRefreshHelper private lateinit var refreshHelper: AutoRefreshHelper
@MockK
private lateinit var sharedPrefProvider: SharedPrefProvider
private val student = getStudentEntity() private val student = getStudentEntity()
private lateinit var messageRepository: MessageRepository private lateinit var messageRepository: MessageRepository
@MockK
private lateinit var moshi: Moshi
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
every { refreshHelper.isShouldBeRefreshed(any()) } returns false 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) @Test(expected = NoSuchElementException::class)