From 1157be18cf3bbd44426f4cdaae35e69b9f3cecca Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 24 Feb 2025 14:18:48 +0100 Subject: [PATCH] Properly handle more than 4 fields in EditProfileViewModel (#4936) Also read `configuration.accounts.max_profile_fields` from `api/v2/instance` to get the correct limit for GoToSocial. Glitch-soc also allows more fields but does not provide configuration yet, see https://github.com/glitch-soc/mastodon/issues/2973 closes https://github.com/tuskyapp/Tusky/issues/3305 --- .../instanceinfo/InstanceInfoRepository.kt | 2 +- .../keylesspalace/tusky/entity/Instance.kt | 6 +- .../tusky/network/MastodonApi.kt | 10 +-- .../tusky/viewmodel/EditProfileViewModel.kt | 79 ++++++++----------- .../components/compose/ComposeActivityTest.kt | 2 +- 5 files changed, 42 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt index 7e8378588..171a0f31b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt @@ -170,7 +170,7 @@ class InstanceInfoRepository @Inject constructor( ?: DEFAULT_IMAGE_MATRIX_LIMIT, maxMediaAttachments = this.configuration?.statuses?.maxMediaAttachments ?: DEFAULT_MAX_MEDIA_ATTACHMENTS, - maxFields = this.pleroma?.metadata?.fieldLimits?.maxFields, + maxFields = this.configuration?.accounts?.maxProfileFields ?: this.pleroma?.metadata?.fieldLimits?.maxFields, maxFieldNameLength = this.pleroma?.metadata?.fieldLimits?.nameLength, maxFieldValueLength = this.pleroma?.metadata?.fieldLimits?.valueLength, translationEnabled = this.configuration?.translation?.enabled diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt index 92e71ba65..1fceb599e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt @@ -51,7 +51,11 @@ data class Instance( data class Urls(@Json(name = "streaming_api") val streamingApi: String? = null) @JsonClass(generateAdapter = true) - data class Accounts(@Json(name = "max_featured_tags") val maxFeaturedTags: Int) + data class Accounts( + @Json(name = "max_featured_tags") val maxFeaturedTags: Int, + // GoToSocial feature + @Json(name = "max_profile_fields") val maxProfileFields: Int? + ) @JsonClass(generateAdapter = true) data class Statuses( diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index a22f35ef3..3042a8fae 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -67,6 +67,7 @@ import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Part +import retrofit2.http.PartMap import retrofit2.http.Path import retrofit2.http.Query @@ -314,14 +315,7 @@ interface MastodonApi { @Part(value = "locked") locked: RequestBody?, @Part avatar: MultipartBody.Part?, @Part header: MultipartBody.Part?, - @Part(value = "fields_attributes[0][name]") fieldName0: RequestBody?, - @Part(value = "fields_attributes[0][value]") fieldValue0: RequestBody?, - @Part(value = "fields_attributes[1][name]") fieldName1: RequestBody?, - @Part(value = "fields_attributes[1][value]") fieldValue1: RequestBody?, - @Part(value = "fields_attributes[2][name]") fieldName2: RequestBody?, - @Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?, - @Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?, - @Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody? + @PartMap fields: Map ): NetworkResult @GET("api/v1/accounts/search") 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 cf92650c0..4f1eadab0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -17,6 +17,7 @@ package com.keylesspalace.tusky.viewmodel import android.app.Application import android.net.Uri +import android.util.Log import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -139,38 +140,38 @@ class EditProfileViewModel @Inject constructor( } viewModelScope.launch { - var avatarFileBody: MultipartBody.Part? = null - diff.avatarFile?.let { - avatarFileBody = MultipartBody.Part.createFormData( + val avatarFileBody: MultipartBody.Part? = diff.avatarFile?.let { + MultipartBody.Part.createFormData( "avatar", randomAlphanumericString(12), it.asRequestBody("image/png".toMediaTypeOrNull()) ) } - var headerFileBody: MultipartBody.Part? = null - diff.headerFile?.let { - headerFileBody = MultipartBody.Part.createFormData( + val headerFileBody: MultipartBody.Part? = diff.headerFile?.let { + MultipartBody.Part.createFormData( "header", randomAlphanumericString(12), it.asRequestBody("image/png".toMediaTypeOrNull()) ) } + val fieldsMap = diff.fields?.let { fields -> + buildMap { + fields.forEachIndexed { index, field -> + put("fields_attributes[$index][name]", field.name.toRequestBody(MultipartBody.FORM)) + put("fields_attributes[$index][value]", field.value.toRequestBody(MultipartBody.FORM)) + } + } + }.orEmpty() + mastodonApi.accountUpdateCredentials( - 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) + displayName = diff.displayName?.toRequestBody(MultipartBody.FORM), + note = diff.note?.toRequestBody(MultipartBody.FORM), + locked = diff.locked?.toString()?.toRequestBody(MultipartBody.FORM), + avatar = avatarFileBody, + header = headerFileBody, + fields = fieldsMap ).fold( { newAccountData -> accountManager.updateAccount(activeAccount, newAccountData) @@ -178,6 +179,7 @@ class EditProfileViewModel @Inject constructor( _saveData.value = Success() }, { throwable -> + Log.d(TAG, "failed updating profile", throwable) _saveData.value = Error(errorMessage = throwable.getServerErrorMessage()) } ) @@ -236,28 +238,13 @@ class EditProfileViewModel @Inject constructor( } // when one field changed, all have to be sent or they unchanged ones would get overridden - 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 DiffProfileData( - displayName, note, locked, field1, field2, field3, field4, headerFile, avatarFile - ) - } - - private fun calculateFieldToUpdate( - newField: StringField?, - fieldsUnchanged: Boolean - ): Pair? { - if (fieldsUnchanged || newField == null) { - return null + val fields = if (oldProfileAccount?.source?.fields == newProfileData.fields) { + null + } else { + newProfileData.fields } - return Pair( - newField.name, - newField.value - ) + + return DiffProfileData(displayName, note, locked, fields, headerFile, avatarFile) } private fun getCacheFileForName(filename: String): File { @@ -268,15 +255,15 @@ class EditProfileViewModel @Inject constructor( val displayName: String?, val note: String?, val locked: Boolean?, - val field1: Pair?, - val field2: Pair?, - val field3: Pair?, - val field4: Pair?, + val fields: List?, val headerFile: File?, val avatarFile: File? ) { fun hasChanges() = displayName != null || note != null || locked != null || - avatarFile != null || headerFile != null || field1 != null || field2 != null || - field3 != null || field4 != null + avatarFile != null || headerFile != null || fields != null + } + + companion object { + private const val TAG = "EditProfileViewModel" } } diff --git a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt index f2a08ca68..d2a5bbf31 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt @@ -599,7 +599,7 @@ class ComposeActivityTest { private fun getConfiguration(maximumStatusCharacters: Int?, charactersReservedPerUrl: Int?): Instance.Configuration { return Instance.Configuration( Instance.Configuration.Urls(), - Instance.Configuration.Accounts(1), + Instance.Configuration.Accounts(maxFeaturedTags = 1, maxProfileFields = 4), Instance.Configuration.Statuses( maximumStatusCharacters ?: InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT, InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS,