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
This commit is contained in:
Konrad Pozniak 2025-02-24 14:18:48 +01:00 committed by GitHub
commit 1157be18cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 42 additions and 57 deletions

View file

@ -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

View file

@ -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(

View file

@ -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<String, RequestBody>
): NetworkResult<Account>
@GET("api/v1/accounts/search")

View file

@ -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<String, String>? {
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<String, String>?,
val field2: Pair<String, String>?,
val field3: Pair<String, String>?,
val field4: Pair<String, String>?,
val fields: List<StringField>?,
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"
}
}

View file

@ -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,