diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index 87df58fb7..f373521ac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -100,6 +100,14 @@ class EditProfileActivity : BaseActivity(), Injectable { } } + private val currentProfileData + get() = ProfileData( + displayName = binding.displayNameEditText.text.toString(), + note = binding.noteEditText.text.toString(), + locked = binding.lockedCheckBox.isChecked, + fields = accountFieldEditAdapter.getFieldData() + ) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -206,14 +214,14 @@ class EditProfileActivity : BaseActivity(), Injectable { } val onBackCallback = object : OnBackPressedCallback(enabled = true) { - override fun handleOnBackPressed() = checkForPotentialUnsavedChanges() + override fun handleOnBackPressed() = checkForUnsavedChanges() } onBackPressedDispatcher.addCallback(this, onBackCallback) } - fun checkForPotentialUnsavedChanges() { - if (hasUnsavedChanges()) { + fun checkForUnsavedChanges() { + if (viewModel.hasUnsavedChanges(currentProfileData)) { showUnsavedChangesDialog() } else { finish() @@ -223,18 +231,10 @@ class EditProfileActivity : BaseActivity(), Injectable { override fun onStop() { super.onStop() if (!isFinishing) { - viewModel.updateProfile(profileData) + viewModel.updateProfile(currentProfileData) } } - private val profileData - get() = ProfileData( - displayName = binding.displayNameEditText.text.toString(), - note = binding.noteEditText.text.toString(), - locked = binding.lockedCheckBox.isChecked, - fields = accountFieldEditAdapter.getFieldData() - ) - private fun observeImage( liveData: LiveData, imageView: ImageView, @@ -308,7 +308,7 @@ class EditProfileActivity : BaseActivity(), Injectable { return super.onOptionsItemSelected(item) } - private fun save() = viewModel.save(profileData) + private fun save() = viewModel.save(currentProfileData) private fun onSaveFailure(msg: String?) { val errorMsg = msg ?: getString(R.string.error_media_upload_sending) @@ -328,8 +328,6 @@ class EditProfileActivity : BaseActivity(), Injectable { } } - private fun hasUnsavedChanges() = viewModel.hasUnsavedChanges(profileData) - private suspend fun launchAlertDialog() = AlertDialog.Builder(this) .setTitle(getString(R.string.title_edit_profile_save_changes_prompt)) .setMessage(getString(R.string.message_edit_profile_save_changes_prompt)) diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt index aa5ff44c8..2e1a8d43c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -42,7 +42,6 @@ import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody -import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import java.io.File @@ -76,7 +75,7 @@ class EditProfileViewModel @Inject constructor( val instanceData: Flow = instanceInfoRepo::getInstanceInfo.asFlow() .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) - private var oldProfileData: Account? = null + private var apiProfileAccount: Account? = null fun obtainProfile() = viewModelScope.launch { if (profileData.value == null || profileData.value is Error) { @@ -84,7 +83,7 @@ class EditProfileViewModel @Inject constructor( mastodonApi.accountVerifyCredentials().fold( { profile -> - oldProfileData = profile + apiProfileAccount = profile profileData.postValue(Success(profile)) }, { @@ -113,21 +112,42 @@ class EditProfileViewModel @Inject constructor( saveData.value = Loading() - val encoded = encodeChangedProfileFields(newProfileData) - if (encoded.allFieldsAreNull()) { - // if nothing has changed, there is no need to make a network request + val diff = getProfileDiff(apiProfileAccount, newProfileData) + if (!diff.hasChanges()) { + // if nothing has changed, there is no need to make an api call saveData.postValue(Success()) return } viewModelScope.launch { + var avatarFileBody: MultipartBody.Part? = null + diff.avatarFile?.let { + avatarFileBody = MultipartBody.Part.createFormData("avatar", randomAlphanumericString(12), it.asRequestBody("image/png".toMediaTypeOrNull())) + } + + var headerFileBody: MultipartBody.Part? = null + diff.headerFile?.let { + headerFileBody = MultipartBody.Part.createFormData("header", randomAlphanumericString(12), it.asRequestBody("image/png".toMediaTypeOrNull())) + } + 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 + diff.displayName?.toRequestBody(MultipartBody.FORM), + diff.note?.toRequestBody(MultipartBody.FORM), + diff.locked?.toString()?.toRequestBody(MultipartBody.FORM), + avatarFileBody, + headerFileBody, + diff.field1?.first?.toRequestBody(MultipartBody.FORM), + diff.field1?.second?.toRequestBody(MultipartBody.FORM), + diff.field2?.first?.toRequestBody(MultipartBody.FORM), + diff.field2?.second?.toRequestBody(MultipartBody.FORM), + diff.field3?.first?.toRequestBody(MultipartBody.FORM), + diff.field3?.second?.toRequestBody(MultipartBody.FORM), + diff.field4?.first?.toRequestBody(MultipartBody.FORM), + diff.field4?.second?.toRequestBody(MultipartBody.FORM) ).fold( - { newProfileData -> + { newAccountData -> saveData.postValue(Success()) - eventHub.dispatch(ProfileEditedEvent(newProfileData)) + eventHub.dispatch(ProfileEditedEvent(newAccountData)) }, { throwable -> saveData.postValue(Error(errorMessage = throwable.getServerErrorMessage())) @@ -151,63 +171,61 @@ class EditProfileViewModel @Inject constructor( } internal fun hasUnsavedChanges(newProfileData: ProfileData): Boolean { - val encoded = encodeChangedProfileFields(newProfileData) - // If all fields are null, there are no changes. - return !encoded.allFieldsAreNull() + val diff = getProfileDiff(apiProfileAccount, newProfileData) + + return diff.hasChanges() } - private fun encodeChangedProfileFields(newProfileData: ProfileData): EncodedProfileData { - val displayName = if (oldProfileData?.displayName == newProfileData.displayName) { + private fun getProfileDiff(oldProfileAccount: Account?, newProfileData: ProfileData): DiffProfileData { + val displayName = if (oldProfileAccount?.displayName == newProfileData.displayName) { null } else { - newProfileData.displayName.toRequestBody(MultipartBody.FORM) + newProfileData.displayName } - val note = if (oldProfileData?.source?.note == newProfileData.note) { + val note = if (oldProfileAccount?.source?.note == newProfileData.note) { null } else { - newProfileData.note.toRequestBody(MultipartBody.FORM) + newProfileData.note } - val locked = if (oldProfileData?.locked == newProfileData.locked) { + val locked = if (oldProfileAccount?.locked == newProfileData.locked) { null } else { - newProfileData.locked.toString().toRequestBody(MultipartBody.FORM) + newProfileData.locked } - val avatar = if (avatarData.value != null) { - val avatarBody = getCacheFileForName(AVATAR_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull()) - MultipartBody.Part.createFormData("avatar", randomAlphanumericString(12), avatarBody) + val avatarFile = if (avatarData.value != null) { + getCacheFileForName(AVATAR_FILE_NAME) } else { null } - val header = if (headerData.value != null) { - val headerBody = getCacheFileForName(HEADER_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull()) - MultipartBody.Part.createFormData("header", randomAlphanumericString(12), headerBody) + val headerFile = if (headerData.value != null) { + getCacheFileForName(HEADER_FILE_NAME) } else { null } // when one field changed, all have to be sent or they unchanged ones would get overridden - 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) + val allFieldsUnchanged = oldProfileAccount?.source?.fields == newProfileData.fields + val field1 = calculateFieldToUpdate(newProfileData.fields.getOrNull(0), allFieldsUnchanged) + val field2 = calculateFieldToUpdate(newProfileData.fields.getOrNull(1), allFieldsUnchanged) + val field3 = calculateFieldToUpdate(newProfileData.fields.getOrNull(2), allFieldsUnchanged) + val field4 = calculateFieldToUpdate(newProfileData.fields.getOrNull(3), allFieldsUnchanged) - return EncodedProfileData( - displayName, note, locked, field1, field2, field3, field4, header, avatar + return DiffProfileData( + displayName, note, locked, field1, field2, field3, field4, headerFile, avatarFile ) } - private fun calculateFieldToUpdate(newField: StringField?, fieldsUnchanged: Boolean): Pair? { + private fun calculateFieldToUpdate(newField: StringField?, fieldsUnchanged: Boolean): Pair? { if (fieldsUnchanged || newField == null) { return null } return Pair( - newField.name.toRequestBody(MultipartBody.FORM), - newField.value.toRequestBody(MultipartBody.FORM) + newField.name, + newField.value ) } @@ -215,19 +233,19 @@ class EditProfileViewModel @Inject constructor( return File(application.cacheDir, filename) } - private data class EncodedProfileData( - val displayName: RequestBody?, - val note: RequestBody?, - val locked: RequestBody?, - val field1: Pair?, - val field2: Pair?, - val field3: Pair?, - val field4: Pair?, - val header: MultipartBody.Part?, - val avatar: MultipartBody.Part? + private data class DiffProfileData( + val displayName: String?, + val note: String?, + val locked: Boolean?, + val field1: Pair?, + val field2: Pair?, + val field3: Pair?, + val field4: Pair?, + val headerFile: File?, + val avatarFile: File? ) { - fun allFieldsAreNull() = displayName == null && note == null && locked == null && - avatar == null && header == null && field1 == null && field2 == null && - field3 == null && field4 == null + fun hasChanges() = displayName != null || note != null || locked != null || + avatarFile != null || headerFile != null || field1 != null || field2 != null || + field3 != null || field4 != null } }