Prompt user before leaving edit profile when any field has been modified.
This commit is contained in:
parent
85f0b1f320
commit
461ec8d722
3 changed files with 124 additions and 64 deletions
|
@ -25,7 +25,9 @@ import android.view.Menu
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -46,9 +48,11 @@ import com.keylesspalace.tusky.di.ViewModelFactory
|
|||
import com.keylesspalace.tusky.util.Error
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.await
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.ProfileData
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
|
@ -200,19 +204,36 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onBackCallback = object : OnBackPressedCallback(enabled = true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (!viewModel.hasUnsavedChanges(gatherProfileData())) finish()
|
||||
|
||||
lifecycleScope.launch {
|
||||
when(showConfirmationDialog()) {
|
||||
AlertDialog.BUTTON_POSITIVE -> save()
|
||||
else -> finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackCallback)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (!isFinishing) {
|
||||
viewModel.updateProfile(
|
||||
binding.displayNameEditText.text.toString(),
|
||||
binding.noteEditText.text.toString(),
|
||||
binding.lockedCheckBox.isChecked,
|
||||
accountFieldEditAdapter.getFieldData()
|
||||
viewModel.updateProfile(gatherProfileData())
|
||||
}
|
||||
}
|
||||
|
||||
private fun gatherProfileData() = ProfileData(
|
||||
displayName = binding.displayNameEditText.text.toString(),
|
||||
note = binding.noteEditText.text.toString(),
|
||||
locked = binding.lockedCheckBox.isChecked,
|
||||
fields = accountFieldEditAdapter.getFieldData(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeImage(
|
||||
liveData: LiveData<Uri>,
|
||||
|
@ -287,14 +308,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun save() {
|
||||
viewModel.save(
|
||||
binding.displayNameEditText.text.toString(),
|
||||
binding.noteEditText.text.toString(),
|
||||
binding.lockedCheckBox.isChecked,
|
||||
accountFieldEditAdapter.getFieldData()
|
||||
)
|
||||
}
|
||||
private fun save() = viewModel.save(gatherProfileData())
|
||||
|
||||
private fun onSaveFailure(msg: String?) {
|
||||
val errorMsg = msg ?: getString(R.string.error_media_upload_sending)
|
||||
|
@ -306,4 +320,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
Log.w("EditProfileActivity", "failed to pick media", throwable)
|
||||
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private suspend fun showConfirmationDialog() = AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.title_edit_profile_save_changes_prompt))
|
||||
.setMessage(getString(R.string.message_edit_profile_save_changes_prompt))
|
||||
.create()
|
||||
.await(R.string.action_save, R.string.action_discard)
|
||||
}
|
||||
|
|
|
@ -51,6 +51,17 @@ import javax.inject.Inject
|
|||
private const val HEADER_FILE_NAME = "header.png"
|
||||
private const val AVATAR_FILE_NAME = "avatar.png"
|
||||
|
||||
|
||||
/**
|
||||
* Conveniently groups Profile Data users can modify in the UI.
|
||||
*/
|
||||
internal data class ProfileData(
|
||||
val displayName: String,
|
||||
val note: String,
|
||||
val locked: Boolean,
|
||||
val fields: List<StringField>,
|
||||
)
|
||||
|
||||
class EditProfileViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub,
|
||||
|
@ -96,29 +107,73 @@ class EditProfileViewModel @Inject constructor(
|
|||
headerData.value = getHeaderUri()
|
||||
}
|
||||
|
||||
fun save(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List<StringField>) {
|
||||
internal fun save(newProfileData: ProfileData) {
|
||||
if (saveData.value is Loading || profileData.value !is Success) {
|
||||
return
|
||||
}
|
||||
|
||||
saveData.value = Loading()
|
||||
|
||||
val displayName = if (oldProfileData?.displayName == newDisplayName) {
|
||||
null
|
||||
} else {
|
||||
newDisplayName.toRequestBody(MultipartBody.FORM)
|
||||
val encoded = encodeChangedProfileFields(newProfileData)
|
||||
if (encoded.allFieldsAreNull()) {
|
||||
// if nothing has changed, there is no need to make a network request
|
||||
saveData.postValue(Success())
|
||||
return
|
||||
}
|
||||
|
||||
val note = if (oldProfileData?.source?.note == newNote) {
|
||||
null
|
||||
} else {
|
||||
newNote.toRequestBody(MultipartBody.FORM)
|
||||
viewModelScope.launch {
|
||||
mastodonApi.accountUpdateCredentials(
|
||||
encoded.displayName, encoded.note, encoded.locked, encoded.avatar, encoded.header,
|
||||
encoded.field1?.first, encoded.field1?.second, encoded.field2?.first, encoded.field2?.second, encoded.field3?.first, encoded.field3?.second, encoded.field4?.first, encoded.field4?.second
|
||||
).fold(
|
||||
{ newProfileData ->
|
||||
saveData.postValue(Success())
|
||||
eventHub.dispatch(ProfileEditedEvent(newProfileData))
|
||||
},
|
||||
{ throwable ->
|
||||
saveData.postValue(Error(errorMessage = throwable.getServerErrorMessage()))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val locked = if (oldProfileData?.locked == newLocked) {
|
||||
// cache activity state for rotation change
|
||||
internal fun updateProfile(newProfileData: ProfileData) {
|
||||
if (profileData.value is Success) {
|
||||
val newProfileSource = profileData.value?.data?.source?.copy(note = newProfileData.note, fields = newProfileData.fields)
|
||||
val newProfile = profileData.value?.data?.copy(
|
||||
displayName = newProfileData.displayName,
|
||||
locked = newProfileData.locked,
|
||||
source = newProfileSource
|
||||
)
|
||||
|
||||
profileData.postValue(Success(newProfile))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun hasUnsavedChanges(newProfileData: ProfileData) : Boolean {
|
||||
val encoded = encodeChangedProfileFields(newProfileData)
|
||||
// If all fields are null, there are no changes.
|
||||
return !encoded.allFieldsAreNull()
|
||||
}
|
||||
|
||||
private fun encodeChangedProfileFields(newProfileData: ProfileData): EncodedProfileData {
|
||||
val displayName = if (oldProfileData?.displayName == newProfileData.displayName) {
|
||||
null
|
||||
} else {
|
||||
newLocked.toString().toRequestBody(MultipartBody.FORM)
|
||||
newProfileData.displayName.toRequestBody(MultipartBody.FORM)
|
||||
}
|
||||
|
||||
val note = if (oldProfileData?.source?.note == newProfileData.note) {
|
||||
null
|
||||
} else {
|
||||
newProfileData.note.toRequestBody(MultipartBody.FORM)
|
||||
}
|
||||
|
||||
val locked = if (oldProfileData?.locked == newProfileData.locked) {
|
||||
null
|
||||
} else {
|
||||
newProfileData.locked.toString().toRequestBody(MultipartBody.FORM)
|
||||
}
|
||||
|
||||
val avatar = if (avatarData.value != null) {
|
||||
|
@ -136,49 +191,16 @@ class EditProfileViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
// when one field changed, all have to be sent or they unchanged ones would get overridden
|
||||
val fieldsUnchanged = oldProfileData?.source?.fields == newFields
|
||||
val field1 = calculateFieldToUpdate(newFields.getOrNull(0), fieldsUnchanged)
|
||||
val field2 = calculateFieldToUpdate(newFields.getOrNull(1), fieldsUnchanged)
|
||||
val field3 = calculateFieldToUpdate(newFields.getOrNull(2), fieldsUnchanged)
|
||||
val field4 = calculateFieldToUpdate(newFields.getOrNull(3), fieldsUnchanged)
|
||||
val fieldsUnchanged = oldProfileData?.source?.fields == newProfileData.fields
|
||||
val field1 = calculateFieldToUpdate(newProfileData.fields.getOrNull(0), fieldsUnchanged)
|
||||
val field2 = calculateFieldToUpdate(newProfileData.fields.getOrNull(1), fieldsUnchanged)
|
||||
val field3 = calculateFieldToUpdate(newProfileData.fields.getOrNull(2), fieldsUnchanged)
|
||||
val field4 = calculateFieldToUpdate(newProfileData.fields.getOrNull(3), fieldsUnchanged)
|
||||
|
||||
if (displayName == null && note == null && locked == null && avatar == null && header == null &&
|
||||
field1 == null && field2 == null && field3 == null && field4 == null
|
||||
) {
|
||||
/** if nothing has changed, there is no need to make a network request */
|
||||
saveData.postValue(Success())
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
mastodonApi.accountUpdateCredentials(
|
||||
displayName, note, locked, avatar, header,
|
||||
field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second
|
||||
).fold(
|
||||
{ newProfileData ->
|
||||
saveData.postValue(Success())
|
||||
eventHub.dispatch(ProfileEditedEvent(newProfileData))
|
||||
},
|
||||
{ throwable ->
|
||||
saveData.postValue(Error(errorMessage = throwable.getServerErrorMessage()))
|
||||
}
|
||||
return EncodedProfileData(
|
||||
displayName, note, locked, field1, field2, field3, field4, header, avatar
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// cache activity state for rotation change
|
||||
fun updateProfile(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List<StringField>) {
|
||||
if (profileData.value is Success) {
|
||||
val newProfileSource = profileData.value?.data?.source?.copy(note = newNote, fields = newFields)
|
||||
val newProfile = profileData.value?.data?.copy(
|
||||
displayName = newDisplayName,
|
||||
locked = newLocked,
|
||||
source = newProfileSource
|
||||
)
|
||||
|
||||
profileData.postValue(Success(newProfile))
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateFieldToUpdate(newField: StringField?, fieldsUnchanged: Boolean): Pair<RequestBody, RequestBody>? {
|
||||
if (fieldsUnchanged || newField == null) {
|
||||
|
@ -193,4 +215,20 @@ class EditProfileViewModel @Inject constructor(
|
|||
private fun getCacheFileForName(filename: String): File {
|
||||
return File(application.cacheDir, filename)
|
||||
}
|
||||
|
||||
private data class EncodedProfileData(
|
||||
val displayName: RequestBody?,
|
||||
val note: RequestBody?,
|
||||
val locked: RequestBody?,
|
||||
val field1: Pair<RequestBody, RequestBody>?,
|
||||
val field2: Pair<RequestBody, RequestBody>?,
|
||||
val field3: Pair<RequestBody, RequestBody>?,
|
||||
val field4: Pair<RequestBody, RequestBody>?,
|
||||
val header: MultipartBody.Part?,
|
||||
val avatar: MultipartBody.Part?,
|
||||
) {
|
||||
fun allFieldsAreNull() = displayName == null && note == null && locked == null
|
||||
&& avatar == null && header == null && field1 == null && field2 == null
|
||||
&& field3 == null && field4 == null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -820,4 +820,6 @@
|
|||
<string name="error_media_playback">Playback failed: %s</string>
|
||||
<string name="dialog_delete_filter_text">Delete filter \'%1$s\'?"</string>
|
||||
<string name="dialog_delete_filter_positive_action">Delete</string>
|
||||
<string name="title_edit_profile_save_changes_prompt">Unsaved Changes</string>
|
||||
<string name="message_edit_profile_save_changes_prompt">Do you want to save your profile changes?</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue