2018-08-15 20:47:09 +02:00
|
|
|
/* Copyright 2018 Conny Duck
|
|
|
|
*
|
|
|
|
* This file is a part of Tusky.
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
|
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
|
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
|
|
* Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
|
|
* see <http://www.gnu.org/licenses>. */
|
|
|
|
|
|
|
|
package com.keylesspalace.tusky.viewmodel
|
|
|
|
|
2022-03-02 20:39:56 +01:00
|
|
|
import android.app.Application
|
2018-08-15 20:47:09 +02:00
|
|
|
import android.net.Uri
|
2022-03-02 20:39:56 +01:00
|
|
|
import androidx.core.net.toUri
|
2021-06-28 21:13:24 +02:00
|
|
|
import androidx.lifecycle.MutableLiveData
|
|
|
|
import androidx.lifecycle.ViewModel
|
2022-04-14 19:49:49 +02:00
|
|
|
import androidx.lifecycle.viewModelScope
|
2022-05-30 20:03:40 +02:00
|
|
|
import at.connyduck.calladapter.networkresult.fold
|
2018-08-15 20:47:09 +02:00
|
|
|
import com.keylesspalace.tusky.appstore.EventHub
|
|
|
|
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
2022-07-26 20:24:50 +02:00
|
|
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo
|
|
|
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
2018-08-15 20:47:09 +02:00
|
|
|
import com.keylesspalace.tusky.entity.Account
|
|
|
|
import com.keylesspalace.tusky.entity.StringField
|
|
|
|
import com.keylesspalace.tusky.network.MastodonApi
|
2021-06-28 21:13:24 +02:00
|
|
|
import com.keylesspalace.tusky.util.Error
|
|
|
|
import com.keylesspalace.tusky.util.Loading
|
|
|
|
import com.keylesspalace.tusky.util.Resource
|
|
|
|
import com.keylesspalace.tusky.util.Success
|
2022-06-30 20:51:05 +02:00
|
|
|
import com.keylesspalace.tusky.util.getServerErrorMessage
|
2021-06-28 21:13:24 +02:00
|
|
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
2022-07-26 20:24:50 +02:00
|
|
|
import kotlinx.coroutines.flow.Flow
|
|
|
|
import kotlinx.coroutines.flow.SharingStarted
|
|
|
|
import kotlinx.coroutines.flow.asFlow
|
|
|
|
import kotlinx.coroutines.flow.shareIn
|
2022-04-14 19:49:49 +02:00
|
|
|
import kotlinx.coroutines.launch
|
2019-07-16 19:36:04 +02:00
|
|
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
2018-08-15 20:47:09 +02:00
|
|
|
import okhttp3.MultipartBody
|
2021-06-28 21:13:24 +02:00
|
|
|
import okhttp3.RequestBody.Companion.asRequestBody
|
|
|
|
import okhttp3.RequestBody.Companion.toRequestBody
|
2018-08-15 20:47:09 +02:00
|
|
|
import java.io.File
|
|
|
|
import javax.inject.Inject
|
|
|
|
|
|
|
|
private const val HEADER_FILE_NAME = "header.png"
|
|
|
|
private const val AVATAR_FILE_NAME = "avatar.png"
|
|
|
|
|
2023-08-24 10:06:25 +02:00
|
|
|
internal data class ProfileDataInUi(
|
2023-08-19 17:36:00 +02:00
|
|
|
val displayName: String,
|
|
|
|
val note: String,
|
|
|
|
val locked: Boolean,
|
2023-08-19 17:57:25 +02:00
|
|
|
val fields: List<StringField>
|
2023-08-19 17:36:00 +02:00
|
|
|
)
|
|
|
|
|
2021-06-28 21:13:24 +02:00
|
|
|
class EditProfileViewModel @Inject constructor(
|
|
|
|
private val mastodonApi: MastodonApi,
|
2022-03-02 20:39:56 +01:00
|
|
|
private val eventHub: EventHub,
|
2022-07-26 20:24:50 +02:00
|
|
|
private val application: Application,
|
|
|
|
private val instanceInfoRepo: InstanceInfoRepository
|
2021-06-28 21:13:24 +02:00
|
|
|
) : ViewModel() {
|
2018-08-15 20:47:09 +02:00
|
|
|
|
|
|
|
val profileData = MutableLiveData<Resource<Account>>()
|
2022-03-02 20:39:56 +01:00
|
|
|
val avatarData = MutableLiveData<Uri>()
|
|
|
|
val headerData = MutableLiveData<Uri>()
|
2018-08-15 20:47:09 +02:00
|
|
|
val saveData = MutableLiveData<Resource<Nothing>>()
|
2022-07-26 20:24:50 +02:00
|
|
|
|
|
|
|
val instanceData: Flow<InstanceInfo> = instanceInfoRepo::getInstanceInfo.asFlow()
|
|
|
|
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
2018-08-15 20:47:09 +02:00
|
|
|
|
2023-08-22 15:52:09 +02:00
|
|
|
private var apiProfileAccount: Account? = null
|
2018-08-15 20:47:09 +02:00
|
|
|
|
2022-04-14 19:49:49 +02:00
|
|
|
fun obtainProfile() = viewModelScope.launch {
|
2021-06-28 21:13:24 +02:00
|
|
|
if (profileData.value == null || profileData.value is Error) {
|
2018-08-15 20:47:09 +02:00
|
|
|
profileData.postValue(Loading())
|
|
|
|
|
2022-04-14 19:49:49 +02:00
|
|
|
mastodonApi.accountVerifyCredentials().fold(
|
|
|
|
{ profile ->
|
2023-08-22 15:52:09 +02:00
|
|
|
apiProfileAccount = profile
|
2022-04-14 19:49:49 +02:00
|
|
|
profileData.postValue(Success(profile))
|
|
|
|
},
|
|
|
|
{
|
|
|
|
profileData.postValue(Error())
|
|
|
|
}
|
|
|
|
)
|
2018-08-15 20:47:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-02 20:39:56 +01:00
|
|
|
fun getAvatarUri() = getCacheFileForName(AVATAR_FILE_NAME).toUri()
|
2018-08-15 20:47:09 +02:00
|
|
|
|
2022-03-02 20:39:56 +01:00
|
|
|
fun getHeaderUri() = getCacheFileForName(HEADER_FILE_NAME).toUri()
|
2018-08-15 20:47:09 +02:00
|
|
|
|
2022-03-02 20:39:56 +01:00
|
|
|
fun newAvatarPicked() {
|
|
|
|
avatarData.value = getAvatarUri()
|
2018-08-15 20:47:09 +02:00
|
|
|
}
|
|
|
|
|
2022-03-02 20:39:56 +01:00
|
|
|
fun newHeaderPicked() {
|
|
|
|
headerData.value = getHeaderUri()
|
2018-08-15 20:47:09 +02:00
|
|
|
}
|
|
|
|
|
2023-08-24 10:06:25 +02:00
|
|
|
internal fun save(newProfileData: ProfileDataInUi) {
|
2021-06-28 21:13:24 +02:00
|
|
|
if (saveData.value is Loading || profileData.value !is Success) {
|
2018-08-15 20:47:09 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-03-02 20:39:56 +01:00
|
|
|
saveData.value = Loading()
|
|
|
|
|
2023-08-22 15:52:09 +02:00
|
|
|
val diff = getProfileDiff(apiProfileAccount, newProfileData)
|
2023-08-22 16:12:03 +02:00
|
|
|
if (!diff.hasChanges()) {
|
2023-08-22 15:52:09 +02:00
|
|
|
// if nothing has changed, there is no need to make an api call
|
2023-08-24 10:06:25 +02:00
|
|
|
saveData.value = Success()
|
2023-08-19 17:36:00 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
viewModelScope.launch {
|
2023-08-22 16:12:03 +02:00
|
|
|
var avatarFileBody: MultipartBody.Part? = null
|
|
|
|
diff.avatarFile?.let {
|
2023-08-23 15:06:08 +02:00
|
|
|
avatarFileBody = MultipartBody.Part.createFormData("avatar", randomAlphanumericString(12), it.asRequestBody("image/png".toMediaTypeOrNull()))
|
2023-08-22 16:12:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
var headerFileBody: MultipartBody.Part? = null
|
|
|
|
diff.headerFile?.let {
|
2023-08-23 15:06:08 +02:00
|
|
|
headerFileBody = MultipartBody.Part.createFormData("header", randomAlphanumericString(12), it.asRequestBody("image/png".toMediaTypeOrNull()))
|
2023-08-22 16:12:03 +02:00
|
|
|
}
|
|
|
|
|
2023-08-19 17:36:00 +02:00
|
|
|
mastodonApi.accountUpdateCredentials(
|
2023-08-22 16:12:03 +02:00
|
|
|
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),
|
2023-08-23 15:06:08 +02:00
|
|
|
diff.field4?.second?.toRequestBody(MultipartBody.FORM)
|
2023-08-19 17:36:00 +02:00
|
|
|
).fold(
|
2023-08-22 15:52:09 +02:00
|
|
|
{ newAccountData ->
|
2023-08-19 17:36:00 +02:00
|
|
|
saveData.postValue(Success())
|
2023-08-22 15:52:09 +02:00
|
|
|
eventHub.dispatch(ProfileEditedEvent(newAccountData))
|
2023-08-19 17:36:00 +02:00
|
|
|
},
|
|
|
|
{ throwable ->
|
|
|
|
saveData.postValue(Error(errorMessage = throwable.getServerErrorMessage()))
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// cache activity state for rotation change
|
2023-08-24 10:06:25 +02:00
|
|
|
internal fun updateProfile(newProfileData: ProfileDataInUi) {
|
2023-08-19 17:36:00 +02:00
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2023-08-24 10:06:25 +02:00
|
|
|
profileData.value = Success(newProfile)
|
2023-08-19 17:36:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-24 10:06:25 +02:00
|
|
|
internal fun hasUnsavedChanges(newProfileData: ProfileDataInUi): Boolean {
|
2023-08-22 15:52:09 +02:00
|
|
|
val diff = getProfileDiff(apiProfileAccount, newProfileData)
|
2023-08-22 16:12:03 +02:00
|
|
|
|
|
|
|
return diff.hasChanges()
|
2023-08-19 17:36:00 +02:00
|
|
|
}
|
|
|
|
|
2023-08-24 10:06:25 +02:00
|
|
|
private fun getProfileDiff(oldProfileAccount: Account?, newProfileData: ProfileDataInUi): DiffProfileData {
|
2023-08-22 15:52:09 +02:00
|
|
|
val displayName = if (oldProfileAccount?.displayName == newProfileData.displayName) {
|
2018-08-15 20:47:09 +02:00
|
|
|
null
|
|
|
|
} else {
|
2023-08-22 16:12:03 +02:00
|
|
|
newProfileData.displayName
|
2018-08-15 20:47:09 +02:00
|
|
|
}
|
|
|
|
|
2023-08-22 15:52:09 +02:00
|
|
|
val note = if (oldProfileAccount?.source?.note == newProfileData.note) {
|
2018-08-15 20:47:09 +02:00
|
|
|
null
|
|
|
|
} else {
|
2023-08-22 16:12:03 +02:00
|
|
|
newProfileData.note
|
2018-08-15 20:47:09 +02:00
|
|
|
}
|
|
|
|
|
2023-08-22 15:52:09 +02:00
|
|
|
val locked = if (oldProfileAccount?.locked == newProfileData.locked) {
|
2018-08-15 20:47:09 +02:00
|
|
|
null
|
|
|
|
} else {
|
2023-08-22 16:12:03 +02:00
|
|
|
newProfileData.locked
|
2018-08-15 20:47:09 +02:00
|
|
|
}
|
|
|
|
|
2023-08-22 16:12:03 +02:00
|
|
|
val avatarFile = if (avatarData.value != null) {
|
|
|
|
getCacheFileForName(AVATAR_FILE_NAME)
|
2018-08-15 20:47:09 +02:00
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
|
2023-08-22 16:12:03 +02:00
|
|
|
val headerFile = if (headerData.value != null) {
|
|
|
|
getCacheFileForName(HEADER_FILE_NAME)
|
2018-08-15 20:47:09 +02:00
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
|
|
|
|
// when one field changed, all have to be sent or they unchanged ones would get overridden
|
2023-08-22 16:12:03 +02:00
|
|
|
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)
|
2023-08-19 17:36:00 +02:00
|
|
|
|
2023-08-22 15:52:09 +02:00
|
|
|
return DiffProfileData(
|
2023-08-22 16:12:03 +02:00
|
|
|
displayName, note, locked, field1, field2, field3, field4, headerFile, avatarFile
|
2023-08-19 17:36:00 +02:00
|
|
|
)
|
2018-08-15 20:47:09 +02:00
|
|
|
}
|
|
|
|
|
2023-08-22 16:12:03 +02:00
|
|
|
private fun calculateFieldToUpdate(newField: StringField?, fieldsUnchanged: Boolean): Pair<String, String>? {
|
2021-06-28 21:13:24 +02:00
|
|
|
if (fieldsUnchanged || newField == null) {
|
2018-08-15 20:47:09 +02:00
|
|
|
return null
|
|
|
|
}
|
|
|
|
return Pair(
|
2023-08-22 16:12:03 +02:00
|
|
|
newField.name,
|
2023-08-23 15:06:08 +02:00
|
|
|
newField.value
|
2018-08-15 20:47:09 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-03-02 20:39:56 +01:00
|
|
|
private fun getCacheFileForName(filename: String): File {
|
|
|
|
return File(application.cacheDir, filename)
|
2018-08-15 20:47:09 +02:00
|
|
|
}
|
2023-08-19 17:36:00 +02:00
|
|
|
|
2023-08-22 15:52:09 +02:00
|
|
|
private data class DiffProfileData(
|
2023-08-22 16:12:03 +02:00
|
|
|
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 headerFile: File?,
|
|
|
|
val avatarFile: File?
|
2023-08-19 17:36:00 +02:00
|
|
|
) {
|
2023-08-22 16:12:03 +02:00
|
|
|
fun hasChanges() = displayName != null || note != null || locked != null ||
|
|
|
|
avatarFile != null || headerFile != null || field1 != null || field2 != null ||
|
|
|
|
field3 != null || field4 != null
|
2023-08-19 17:36:00 +02:00
|
|
|
}
|
2021-06-28 21:13:24 +02:00
|
|
|
}
|