Improve prompts when draft is empty (#3699)

When the user is closing the compose view,

    if it's new and empty, don't show a prompt.
    if it's an existing draft and now empty, ask if the user wants to delete it or continue editing. I don't think there is much value in saving an empty draft.
---------
This commit is contained in:
Prat 2023-06-13 06:45:17 -07:00 committed by GitHub
parent eedf6abf91
commit 485a4c364e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 69 additions and 13 deletions

View File

@ -78,6 +78,7 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.EmojiAdapter
import com.keylesspalace.tusky.adapter.LocaleAdapter import com.keylesspalace.tusky.adapter.LocaleAdapter
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
import com.keylesspalace.tusky.components.compose.ComposeViewModel.ConfirmationKind
import com.keylesspalace.tusky.components.compose.dialog.CaptionDialog import com.keylesspalace.tusky.components.compose.dialog.CaptionDialog
import com.keylesspalace.tusky.components.compose.dialog.makeFocusDialog import com.keylesspalace.tusky.components.compose.dialog.makeFocusDialog
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
@ -1126,17 +1127,20 @@ class ComposeActivity :
private fun handleCloseButton() { private fun handleCloseButton() {
val contentText = binding.composeEditField.text.toString() val contentText = binding.composeEditField.text.toString()
val contentWarning = binding.composeContentWarningField.text.toString() val contentWarning = binding.composeContentWarningField.text.toString()
if (viewModel.didChange(contentText, contentWarning)) { when (viewModel.handleCloseButton(contentText, contentWarning)) {
when (viewModel.composeKind) { ConfirmationKind.NONE -> {
ComposeKind.NEW -> getSaveAsDraftOrDiscardDialog(contentText, contentWarning)
ComposeKind.EDIT_DRAFT -> getUpdateDraftOrDiscardDialog(contentText, contentWarning)
ComposeKind.EDIT_POSTED -> getContinueEditingOrDiscardDialog()
ComposeKind.EDIT_SCHEDULED -> getContinueEditingOrDiscardDialog()
}.show()
} else {
viewModel.stopUploads() viewModel.stopUploads()
finishWithoutSlideOutAnimation() finishWithoutSlideOutAnimation()
} }
ConfirmationKind.SAVE_OR_DISCARD ->
getSaveAsDraftOrDiscardDialog(contentText, contentWarning).show()
ConfirmationKind.UPDATE_OR_DISCARD ->
getUpdateDraftOrDiscardDialog(contentText, contentWarning).show()
ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_CHANGES ->
getContinueEditingOrDiscardDialog().show()
ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_DRAFT ->
getDeleteEmptyDraftOrContinueEditing().show()
}
} }
/** /**
@ -1200,6 +1204,23 @@ class ComposeActivity :
} }
} }
/**
* User is editing an existing draft and making it empty.
* The user can either delete the empty draft or go back to editing.
*/
private fun getDeleteEmptyDraftOrContinueEditing(): AlertDialog.Builder {
return AlertDialog.Builder(this)
.setMessage(R.string.compose_delete_draft)
.setPositiveButton(R.string.action_delete) { _, _ ->
viewModel.deleteDraft()
viewModel.stopUploads()
finishWithoutSlideOutAnimation()
}
.setNegativeButton(R.string.action_continue_edit) { _, _ ->
// Do nothing, dialog will dismiss, user can continue editing
}
}
private fun deleteDraftAndFinish() { private fun deleteDraftAndFinish() {
viewModel.deleteDraft() viewModel.deleteDraft()
finishWithoutSlideOutAnimation() finishWithoutSlideOutAnimation()

View File

@ -21,6 +21,7 @@ import androidx.core.net.toUri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import at.connyduck.calladapter.networkresult.fold import at.connyduck.calladapter.networkresult.fold
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeKind
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter.AutocompleteResult import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter.AutocompleteResult
import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.components.drafts.DraftHelper
@ -94,7 +95,7 @@ class ComposeViewModel @Inject constructor(
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList()) val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
val uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) val uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
lateinit var composeKind: ComposeActivity.ComposeKind lateinit var composeKind: ComposeKind
// Used in ComposeActivity to pass state to result function when cropImage contract inflight // Used in ComposeActivity to pass state to result function when cropImage contract inflight
var cropImageItemOld: QueuedMedia? = null var cropImageItemOld: QueuedMedia? = null
@ -213,7 +214,28 @@ class ComposeViewModel @Inject constructor(
this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true
} }
fun didChange(content: String?, contentWarning: String?): Boolean { fun handleCloseButton(contentText: String?, contentWarning: String?): ConfirmationKind {
return if (didChange(contentText, contentWarning)) {
when (composeKind) {
ComposeKind.NEW -> if (isEmpty(contentText, contentWarning)) {
ConfirmationKind.NONE
} else {
ConfirmationKind.SAVE_OR_DISCARD
}
ComposeKind.EDIT_DRAFT -> if (isEmpty(contentText, contentWarning)) {
ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_DRAFT
} else {
ConfirmationKind.UPDATE_OR_DISCARD
}
ComposeKind.EDIT_POSTED -> ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_CHANGES
ComposeKind.EDIT_SCHEDULED -> ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_CHANGES
}
} else {
ConfirmationKind.NONE
}
}
private fun didChange(content: String?, contentWarning: String?): Boolean {
val textChanged = content.orEmpty() != startingText.orEmpty() val textChanged = content.orEmpty() != startingText.orEmpty()
val contentWarningChanged = contentWarning.orEmpty() != startingContentWarning val contentWarningChanged = contentWarning.orEmpty() != startingContentWarning
val mediaChanged = media.value.isNotEmpty() val mediaChanged = media.value.isNotEmpty()
@ -223,6 +245,10 @@ class ComposeViewModel @Inject constructor(
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange
} }
private fun isEmpty(content: String?, contentWarning: String?): Boolean {
return !modifiedInitialState && (content.isNullOrBlank() && contentWarning.isNullOrBlank() && media.value.isEmpty() && poll.value == null)
}
fun contentWarningChanged(value: Boolean) { fun contentWarningChanged(value: Boolean) {
showContentWarning.value = value showContentWarning.value = value
contentWarningStateChanged = true contentWarningStateChanged = true
@ -390,7 +416,7 @@ class ComposeViewModel @Inject constructor(
return return
} }
composeKind = composeOptions?.kind ?: ComposeActivity.ComposeKind.NEW composeKind = composeOptions?.kind ?: ComposeKind.NEW
val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy
@ -486,6 +512,14 @@ class ComposeViewModel @Inject constructor(
private companion object { private companion object {
const val TAG = "ComposeViewModel" const val TAG = "ComposeViewModel"
} }
enum class ConfirmationKind {
NONE, // just close
SAVE_OR_DISCARD,
UPDATE_OR_DISCARD,
CONTINUE_EDITING_OR_DISCARD_CHANGES, // editing post
CONTINUE_EDITING_OR_DISCARD_DRAFT // edit draft
}
} }
/** /**

View File

@ -481,6 +481,7 @@
<string name="action_remove">Remove</string> <string name="action_remove">Remove</string>
<string name="lock_account_label">Lock account</string> <string name="lock_account_label">Lock account</string>
<string name="lock_account_label_description">Requires you to manually approve followers</string> <string name="lock_account_label_description">Requires you to manually approve followers</string>
<string name="compose_delete_draft">Delete draft?</string>
<string name="compose_save_draft">Save draft?</string> <string name="compose_save_draft">Save draft?</string>
<string name="compose_save_draft_loses_media">Save draft? (Attachments will be uploaded again when you restore the draft.)</string> <string name="compose_save_draft_loses_media">Save draft? (Attachments will be uploaded again when you restore the draft.)</string>
<string name="compose_unsaved_changes">You have unsaved changes.</string> <string name="compose_unsaved_changes">You have unsaved changes.</string>