PixelDroid-App-Android/app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt

307 lines
11 KiB
Kotlin

package org.pixeldroid.app.profile
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import android.text.Editable
import android.util.Log
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import okhttp3.MultipartBody
import org.pixeldroid.app.postCreation.ProgressRequestBody
import org.pixeldroid.app.posts.fromHtml
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.updateUserInfoDb
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import retrofit2.HttpException
import javax.inject.Inject
@HiltViewModel
class EditProfileViewModel @Inject constructor(
@ApplicationContext private val applicationContext: Context
): ViewModel() {
@Inject
lateinit var apiHolder: PixelfedAPIHolder
@Inject
lateinit var db: AppDatabase
private val _uiState = MutableStateFlow(EditProfileActivityUiState())
val uiState: StateFlow<EditProfileActivityUiState> = _uiState
private var oldProfile: Account? = null
var submittedChanges = false
private set
init {
loadProfile()
}
private fun loadProfile() {
viewModelScope.launch {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
try {
val profile = api.verifyCredentials()
updateUserInfoDb(db, profile)
if (oldProfile == null) oldProfile = profile
_uiState.update { currentUiState ->
currentUiState.copy(
name = oldProfile?.display_name,
bio = oldProfile?.source?.note,
profilePictureUri = oldProfile?.anyAvatar()?.toUri(),
privateAccount = oldProfile?.locked,
loadingProfile = false,
sendingProfile = false,
profileLoaded = true,
error = false
)
}
} catch (exception: Exception) {
_uiState.update { currentUiState ->
currentUiState.copy(
sendingProfile = false,
profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = true
)
}
}
}
}
fun sendProfile() {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
_uiState.update { currentUiState ->
currentUiState.copy(
sendingProfile = true,
profileSent = false,
error = false
)
}
viewModelScope.launch {
with(uiState.value) {
try {
val account = api.updateCredentials(
displayName = name,
note = bio,
locked = privateAccount,
)
if (madeChanges()) submittedChanges = true
oldProfile = account
_uiState.update { currentUiState ->
currentUiState.copy(
bio = account.source?.note
?: account.note?.let { fromHtml(it).toString() },
name = account.display_name,
profilePictureUri = if (profilePictureChanged) profilePictureUri
else account.anyAvatar()?.toUri(),
uploadProgress = 0,
uploadingPicture = profilePictureChanged,
privateAccount = account.locked,
sendingProfile = false,
profileSent = true,
loadingProfile = false,
profileLoaded = true,
error = false
)
}
if(profilePictureChanged) uploadImage()
} catch (exception: Exception) {
Log.e("TAG", exception.toString())
_uiState.update { currentUiState ->
currentUiState.copy(
sendingProfile = false,
profileSent = false,
error = true
)
}
}
}
}
}
fun updateBio(bio: Editable?) {
_uiState.update { currentUiState ->
currentUiState.copy(bio = bio.toString())
}
}
fun updateName(name: Editable?) {
_uiState.update { currentUiState ->
currentUiState.copy(name = name.toString())
}
}
fun updatePrivate(isChecked: Boolean) {
_uiState.update { currentUiState ->
currentUiState.copy(privateAccount = isChecked)
}
}
fun madeChanges(): Boolean =
with(uiState.value) {
val privateChanged = oldProfile?.locked != privateAccount
val displayNameChanged = oldProfile?.display_name != name
val bioChanged: Boolean = oldProfile?.source?.note?.let { it != bio }
// If source note is null, check note
?: oldProfile?.note?.let { fromHtml(it).toString() != bio }
?: true
profilePictureChanged || privateChanged || displayNameChanged || bioChanged
}
fun clickedCard() {
if (uiState.value.error) {
if (!uiState.value.profileLoaded) {
// Load failed
loadProfile()
} else if (uiState.value.profileLoaded) {
// Send failed
sendProfile()
}
} else {
// Dismiss success card
_uiState.update { currentUiState ->
currentUiState.copy(profileSent = false)
}
}
}
fun updateImage(image: String) {
_uiState.update { currentUiState ->
currentUiState.copy(
profilePictureUri = image.toUri(),
profilePictureChanged = true,
profileSent = false
)
}
}
private fun uploadImage() {
val image = uiState.value.profilePictureUri!!
val inputStream =
applicationContext.contentResolver.openInputStream(image)
?: return
val size: Long =
if (image.scheme == "content") {
applicationContext.contentResolver.query(
image,
null,
null,
null,
null
)
?.use { cursor ->
/* Get the column indexes of the data in the Cursor,
* move to the first row in the Cursor, get the data,
* and display it.
*/
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
cursor.getLong(sizeIndex)
} ?: 0
} else {
image.toFile().length()
}
val imagePart = ProgressRequestBody(inputStream, size, "image/*")
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("avatar", System.currentTimeMillis().toString(), imagePart)
.build()
val sub = imagePart.progressSubject
.subscribeOn(Schedulers.io())
.subscribe { percentage ->
_uiState.update { currentUiState ->
currentUiState.copy(
uploadProgress = percentage.toInt()
)
}
}
var postSub: Disposable? = null
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val pixelfed = db.instanceDao().getActiveInstance().pixelfed
val inter =
if(pixelfed) api.updateProfilePicture(requestBody.parts[0])
else api.updateProfilePictureMastodon(requestBody.parts[0])
postSub = inter
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
/* onNext = */ { account: Account ->
account.anyAvatar()?.let {
_uiState.update { currentUiState ->
currentUiState.copy(
profilePictureUri = it.toUri()
)
}
}
},
/* onError = */ { e: Throwable ->
Log.e("error", (e as? HttpException)?.message().orEmpty())
_uiState.update { currentUiState ->
currentUiState.copy(
uploadProgress = 0,
uploadingPicture = false,
error = true
)
}
e.printStackTrace()
postSub?.dispose()
sub.dispose()
},
/* onComplete = */ {
_uiState.update { currentUiState ->
currentUiState.copy(
profilePictureChanged = false,
uploadProgress = 100,
uploadingPicture = false
)
}
postSub?.dispose()
sub.dispose()
}
)
}
}
data class EditProfileActivityUiState(
val name: String? = null,
val bio: String? = null,
val profilePictureUri: Uri? = null,
val profilePictureChanged: Boolean = false,
val privateAccount: Boolean? = null,
val loadingProfile: Boolean = true,
val profileLoaded: Boolean = false,
val sendingProfile: Boolean = false,
val profileSent: Boolean = false,
val error: Boolean = false,
val uploadingPicture: Boolean = false,
val uploadProgress: Int = 0,
)