From e35cb17879b901691c74d4112123f86c66fb4916 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Thu, 23 Jun 2022 17:11:11 +0200 Subject: [PATCH 1/2] start on profile editing functionality --- app/src/main/AndroidManifest.xml | 4 +- .../app/profile/EditProfileActivity.kt | 71 ++++++++++ .../app/profile/EditProfileViewModel.kt | 129 ++++++++++++++++++ .../pixeldroid/app/profile/ProfileActivity.kt | 8 +- .../pixeldroid/app/utils/api/PixelfedAPI.kt | 8 ++ .../pixeldroid/app/utils/api/objects/Field.kt | 9 +- .../app/utils/api/objects/Source.kt | 14 +- .../app/utils/di/ApplicationComponent.kt | 2 + app/src/main/res/drawable/done.xml | 5 + .../main/res/layout/activity_edit_profile.xml | 69 ++++++++++ app/src/main/res/layout/activity_login.xml | 3 +- app/src/main/res/menu/edit_profile_menu.xml | 11 ++ app/src/main/res/values/strings.xml | 1 + 13 files changed, 324 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/org/pixeldroid/app/profile/EditProfileActivity.kt create mode 100644 app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt create mode 100644 app/src/main/res/drawable/done.xml create mode 100644 app/src/main/res/layout/activity_edit_profile.xml create mode 100644 app/src/main/res/menu/edit_profile_menu.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 128d4462..b77f5f17 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,7 +31,9 @@ android:name=".posts.AlbumActivity" android:exported="false" android:theme="@style/AppTheme.ActionBar.Transparent"/> - + + binding.savingProgressBar.visibility = if(uiState.loadingProfile) View.VISIBLE else View.INVISIBLE + binding.bioEditText.setText(uiState.bio) + binding.nameEditText.setText(uiState.name) + Glide.with(binding.profilePic).load(uiState.profilePictureUri) + .apply(RequestOptions.circleCropTransform()) + .into(binding.profilePic) + if(uiState.error){ + Snackbar.make(binding.root, "Something went wrong", + Snackbar.LENGTH_LONG).show() + model.errorShown() + } + } + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.edit_profile_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId){ + R.id.action_apply -> { + model.apply( + binding.nameEditText.text.toString(), + binding.bioEditText.text.toString(), + ) + return true + } + } + return super.onOptionsItemSelected(item) + } +} diff --git a/app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt b/app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt new file mode 100644 index 00000000..a225d398 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt @@ -0,0 +1,129 @@ +package org.pixeldroid.app.profile + +import android.app.Application +import android.net.Uri +import android.util.Log +import androidx.core.net.toUri +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.toRequestBody +import org.pixeldroid.app.utils.PixelDroidApplication +import org.pixeldroid.app.utils.api.objects.Account +import org.pixeldroid.app.utils.di.PixelfedAPIHolder +import retrofit2.HttpException +import java.io.IOException +import java.lang.Exception +import javax.inject.Inject + +class EditProfileViewModel(application: Application) : AndroidViewModel(application) { + + @Inject + lateinit var apiHolder: PixelfedAPIHolder + + private val _uiState = MutableStateFlow(EditProfileActivityUiState()) + val uiState: StateFlow = _uiState + + var oldProfile: Account? = null + + init { + (application as PixelDroidApplication).getAppComponent().inject(this) + loadProfile() + } + + private fun loadProfile() { + viewModelScope.launch { + val api = apiHolder.api ?: apiHolder.setToCurrentUser() + try { + oldProfile = api.verifyCredentials() + _uiState.update { currentUiState -> + currentUiState.copy( + name = oldProfile?.display_name, + bio = oldProfile?.source?.note, + profilePictureUri = oldProfile?.anyAvatar()?.toUri(), + privateAccount = oldProfile?.locked, + loadingProfile = false, + sendingProfile = false + ) + } + } catch (exception: IOException) { + _uiState.update { currentUiState -> + currentUiState.copy( + ) + } + } catch (exception: HttpException) { + _uiState.update { currentUiState -> + currentUiState.copy( + ) + } + } + } + } + + fun apply(name: String, bio: String) { + //TODO check if name and bio have changed, else send null to updatecredentials or don't update at all + _uiState.update { currentUiState -> + if(oldProfile != null) currentUiState.copy(name = name, bio = bio, sendingProfile = true, loadingProfile = false) + else currentUiState.copy(name = name, bio = bio, sendingProfile = false) + } + if(oldProfile == null) return + + val api = apiHolder.api ?: apiHolder.setToCurrentUser() + + val requestBody = null //MultipartBody.Part.createFormData("avatar", System.currentTimeMillis().toString(), avatarBody) + + viewModelScope.launch { + with(uiState.value) { + try { + api.updateCredentials( + displayName = name, + note = bio, + locked = privateAccount, + // avatar = requestBody + ) + } catch (exception: IOException) { + Log.e("TAG", exception.toString()) + _uiState.update { currentUiState -> + currentUiState.copy(error = true) + } + } catch (exception: HttpException) { + Log.e("TAG", exception.toString()) + _uiState.update { currentUiState -> + currentUiState.copy(error = true) + } + } catch (exception: Exception) { + Log.e("TAG", exception.toString()) + + } + } + } + } + + fun errorShown() { + _uiState.update { currentUiState -> + currentUiState.copy(error = false) + } + } +} + +data class EditProfileActivityUiState( + val name: String? = null, + val bio: String? = null, + val profilePictureUri: Uri?= null, + val privateAccount: Boolean? = null, + val loadingProfile: Boolean = true, + val sendingProfile: Boolean = false, + val error: Boolean = false +) + +class EditProfileViewModelFactory(val application: Application) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return modelClass.getConstructor(Application::class.java).newInstance(application) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/profile/ProfileActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/ProfileActivity.kt index d85b9a08..cedc21fb 100644 --- a/app/src/main/java/org/pixeldroid/app/profile/ProfileActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/profile/ProfileActivity.kt @@ -228,12 +228,8 @@ class ProfileActivity : BaseThemedWithBarActivity() { } private fun onClickEditButton() { - val url = "$domain/settings/home" - - if(!openUrl(url)) { - Snackbar.make(binding.root, getString(R.string.edit_link_failed), - Snackbar.LENGTH_LONG).show() - } + val intent = Intent(this, EditProfileActivity::class.java) + ContextCompat.startActivity(this, intent, null) } private fun onClickFollowers(account: Account?) { diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt index 2d8c11df..2c62f4a7 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt @@ -295,6 +295,14 @@ interface PixelfedAPI { @Header("Authorization") authorization: String? = null ): Account + @Multipart + @PATCH("/api/v1/accounts/update_credentials") + suspend fun updateCredentials( + @Part(value = "display_name") displayName: String?, + @Part(value = "note") note: String?, + @Part(value = "locked") locked: Boolean?, + // @Part avatar: MultipartBody.Part?, + ): Account @GET("/api/v1/accounts/{id}/statuses") suspend fun accountPosts( diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Field.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Field.kt index f7ad2d38..711e4e59 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Field.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Field.kt @@ -1,5 +1,12 @@ package org.pixeldroid.app.utils.api.objects import java.io.Serializable +import java.time.Instant -class Field: Serializable +data class Field( + //Required attributes + val name: String?, + val value: String?, + //Optional attributes + val verified_at: Instant? +): Serializable diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Source.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Source.kt index 8430be4e..0c01533e 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Source.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Source.kt @@ -2,4 +2,16 @@ package org.pixeldroid.app.utils.api.objects import java.io.Serializable -class Source: Serializable +data class Source( + val note: String?, + val fields: List?, + //Nullable attributes + val privacy: Privacy?, + val sensitive: Boolean?, + val language: String?, //ISO 639-1 language two-letter code + val follow_requests_count: Int?, +): Serializable { + enum class Privacy: Serializable { + public, unlisted, private, direct + } +} diff --git a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt index 0a646b08..b9a779bb 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt @@ -8,6 +8,7 @@ import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.BaseFragment import dagger.Component import org.pixeldroid.app.postCreation.PostCreationViewModel +import org.pixeldroid.app.profile.EditProfileViewModel import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker import javax.inject.Singleton @@ -20,6 +21,7 @@ interface ApplicationComponent { fun inject(feedFragment: BaseFragment) fun inject(notificationsWorker: NotificationsWorker) fun inject(postCreationViewModel: PostCreationViewModel) + fun inject(editProfileViewModel: EditProfileViewModel) val context: Context? val application: Application? diff --git a/app/src/main/res/drawable/done.xml b/app/src/main/res/drawable/done.xml new file mode 100644 index 00000000..2728880b --- /dev/null +++ b/app/src/main/res/drawable/done.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_profile.xml new file mode 100644 index 00000000..bb0467de --- /dev/null +++ b/app/src/main/res/layout/activity_edit_profile.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index c006d836..04b5c54d 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -33,8 +33,9 @@ android:layout_height="wrap_content" android:gravity="center" android:hint="@string/domain_of_your_instance" + android:visibility="gone" app:errorEnabled="true" - android:visibility="gone"> + tools:visibility="visible"> + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 30aa2c57..a24a9e03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -309,4 +309,5 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" %d reply %d replies + Save \ No newline at end of file From 2497504530bc5f35956f42bbe7c8585b07f9e915 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Sun, 30 Oct 2022 20:51:09 +0100 Subject: [PATCH 2/2] Basic profile editing --- app/build.gradle | 1 + .../app/profile/EditProfileActivity.kt | 109 ++++++++- .../app/profile/EditProfileViewModel.kt | 224 ++++++++++++++++-- .../pixeldroid/app/utils/api/PixelfedAPI.kt | 14 +- app/src/main/res/drawable/done.xml | 4 +- .../main/res/layout/activity_edit_profile.xml | 129 +++++++++- app/src/main/res/layout/activity_profile.xml | 1 - app/src/main/res/layout/color_dialog.xml | 2 +- app/src/main/res/values/strings.xml | 12 + gradle/verification-metadata.xml | 24 ++ 10 files changed, 476 insertions(+), 44 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7b9f597c..58c480f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,6 +9,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply plugin: 'jacoco' + // Force latest version of Jacoco, initially done to resolve https://github.com/jacoco/jacoco/issues/1155 jacoco.toolVersion = "0.8.7" diff --git a/app/src/main/java/org/pixeldroid/app/profile/EditProfileActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/EditProfileActivity.kt index 8970474a..b3dbcaf0 100644 --- a/app/src/main/java/org/pixeldroid/app/profile/EditProfileActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/profile/EditProfileActivity.kt @@ -1,21 +1,26 @@ package org.pixeldroid.app.profile +import android.app.Activity +import android.app.AlertDialog +import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels +import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions import com.google.android.material.snackbar.Snackbar -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.pixeldroid.app.R import org.pixeldroid.app.databinding.ActivityEditProfileBinding import org.pixeldroid.app.utils.BaseActivity +import org.pixeldroid.app.utils.openUrl class EditProfileActivity : BaseActivity() { @@ -35,20 +40,91 @@ class EditProfileActivity : BaseActivity() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { model.uiState.collect { uiState -> - binding.savingProgressBar.visibility = if(uiState.loadingProfile) View.VISIBLE else View.INVISIBLE - binding.bioEditText.setText(uiState.bio) - binding.nameEditText.setText(uiState.name) + if(uiState.profileLoaded){ + binding.bioEditText.setText(uiState.bio) + binding.nameEditText.setText(uiState.name) + model.changesApplied() + } + binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.profileSent || uiState.error) View.VISIBLE else View.INVISIBLE + if(uiState.loadingProfile) binding.progressText.setText(R.string.fetching_profile) + else if(uiState.sendingProfile) binding.progressText.setText(R.string.saving_profile) + binding.privateSwitch.isChecked = uiState.privateAccount == true Glide.with(binding.profilePic).load(uiState.profilePictureUri) .apply(RequestOptions.circleCropTransform()) .into(binding.profilePic) - if(uiState.error){ - Snackbar.make(binding.root, "Something went wrong", - Snackbar.LENGTH_LONG).show() - model.errorShown() + + binding.savingProgressBar.visibility = if(uiState.error || uiState.profileSent) View.GONE + else View.VISIBLE + + if(uiState.profileSent){ + binding.progressText.setText(R.string.profile_saved) + binding.done.visibility = View.VISIBLE + } else { + binding.done.visibility = View.GONE } + if(uiState.error){ + binding.progressText.setText(R.string.error_profile) + binding.error.visibility = View.VISIBLE + } else binding.error.visibility = View.GONE + } } } + binding.bioEditText.doAfterTextChanged { + model.updateBio(binding.bioEditText.text) + } + binding.nameEditText.doAfterTextChanged { + model.updateName(binding.nameEditText.text) + } + binding.privateSwitch.setOnCheckedChangeListener { _, isChecked -> + model.updatePrivate(isChecked) + } + + binding.progressCard.setOnClickListener { + model.clickedCard() + } + + binding.editButton.setOnClickListener { + val domain = db.userDao().getActiveUser()!!.instance_uri + val url = "$domain/settings/home" + + if(!openUrl(url)) { + Snackbar.make(binding.root, getString(R.string.edit_link_failed), + Snackbar.LENGTH_LONG).show() + } + } + +// binding.changeImageButton.setOnClickListener { +// Intent(Intent.ACTION_GET_CONTENT).apply { +// type = "*/*" +// putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*")) +// action = Intent.ACTION_GET_CONTENT +// addCategory(Intent.CATEGORY_OPENABLE) +// putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) +// uploadImageResultContract.launch( +// Intent.createChooser(this, null) +// ) +// } +// } + } + + private val uploadImageResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val data: Intent? = result.data + if (result.resultCode == Activity.RESULT_OK && data != null) { + val images: ArrayList = ArrayList() + val clipData = data.clipData + if (clipData != null) { + val count = clipData.itemCount + for (i in 0 until count) { + val imageUri: String = clipData.getItemAt(i).uri.toString() + images.add(imageUri) + } + model.uploadImage(images.first()) + } else if (data.data != null) { + images.add(data.data!!.toString()) + model.uploadImage(images.first()) + } + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -56,13 +132,22 @@ class EditProfileActivity : BaseActivity() { return true } + @Deprecated("Deprecated in Java") + override fun onBackPressed() { + if(model.madeChanges()){ + AlertDialog.Builder(binding.root.context).apply { + setMessage(getString(R.string.profile_save_changes)) + setNegativeButton(android.R.string.cancel) { _, _ -> } + setPositiveButton(android.R.string.ok) { _, _ -> super.onBackPressed()} + }.show() + } + else super.onBackPressed() + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId){ R.id.action_apply -> { - model.apply( - binding.nameEditText.text.toString(), - binding.bioEditText.text.toString(), - ) + model.sendProfile() return true } } diff --git a/app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt b/app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt index a225d398..f0613ec5 100644 --- a/app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt @@ -2,24 +2,38 @@ package org.pixeldroid.app.profile import android.app.Application import android.net.Uri +import android.provider.OpenableColumns +import android.text.Editable import android.util.Log +import android.widget.Toast +import androidx.core.net.toFile import androidx.core.net.toUri import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +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.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody -import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.RequestBody +import org.pixeldroid.app.R +import org.pixeldroid.app.postCreation.ProgressRequestBody import org.pixeldroid.app.utils.PixelDroidApplication import org.pixeldroid.app.utils.api.objects.Account +import org.pixeldroid.app.utils.api.objects.Attachment import org.pixeldroid.app.utils.di.PixelfedAPIHolder import retrofit2.HttpException +import java.io.File import java.io.IOException import java.lang.Exception +import java.net.URI import javax.inject.Inject class EditProfileViewModel(application: Application) : AndroidViewModel(application) { @@ -41,7 +55,8 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat viewModelScope.launch { val api = apiHolder.api ?: apiHolder.setToCurrentUser() try { - oldProfile = api.verifyCredentials() + val profile = api.verifyCredentials() + if (oldProfile == null) oldProfile = profile _uiState.update { currentUiState -> currentUiState.copy( name = oldProfile?.display_name, @@ -49,53 +64,94 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat profilePictureUri = oldProfile?.anyAvatar()?.toUri(), privateAccount = oldProfile?.locked, loadingProfile = false, - sendingProfile = false + sendingProfile = false, + profileLoaded = true, + error = false ) } } catch (exception: IOException) { _uiState.update { currentUiState -> currentUiState.copy( + sendingProfile = false, + profileSent = false, + loadingProfile = false, + profileLoaded = false, + error = true ) } } catch (exception: HttpException) { _uiState.update { currentUiState -> currentUiState.copy( + sendingProfile = false, + profileSent = false, + loadingProfile = false, + profileLoaded = false, + error = true ) } } } } - fun apply(name: String, bio: String) { - //TODO check if name and bio have changed, else send null to updatecredentials or don't update at all - _uiState.update { currentUiState -> - if(oldProfile != null) currentUiState.copy(name = name, bio = bio, sendingProfile = true, loadingProfile = false) - else currentUiState.copy(name = name, bio = bio, sendingProfile = false) - } - if(oldProfile == null) return - + fun sendProfile() { val api = apiHolder.api ?: apiHolder.setToCurrentUser() - val requestBody = null //MultipartBody.Part.createFormData("avatar", System.currentTimeMillis().toString(), avatarBody) + val requestBody = + null //MultipartBody.Part.createFormData("avatar", System.currentTimeMillis().toString(), avatarBody) + + _uiState.update { currentUiState -> + currentUiState.copy( + sendingProfile = true, + profileSent = false, + loadingProfile = false, + profileLoaded = false, + error = false + ) + } viewModelScope.launch { with(uiState.value) { try { - api.updateCredentials( + val account = api.updateCredentials( displayName = name, note = bio, locked = privateAccount, - // avatar = requestBody ) + oldProfile = account + _uiState.update { currentUiState -> + currentUiState.copy( + bio = account.note, + name = account.display_name, + profilePictureUri = account.anyAvatar()?.toUri(), + privateAccount = account.locked, + sendingProfile = false, + profileSent = true, + loadingProfile = false, + profileLoaded = true, + error = false + ) + } } catch (exception: IOException) { Log.e("TAG", exception.toString()) _uiState.update { currentUiState -> - currentUiState.copy(error = true) + currentUiState.copy( + sendingProfile = false, + profileSent = false, + loadingProfile = false, + profileLoaded = false, + error = true + ) } } catch (exception: HttpException) { Log.e("TAG", exception.toString()) _uiState.update { currentUiState -> - currentUiState.copy(error = true) + currentUiState.copy( + sendingProfile = false, + profileSent = false, + loadingProfile = false, + profileLoaded = false, + error = true + ) } } catch (exception: Exception) { Log.e("TAG", exception.toString()) @@ -110,16 +166,150 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat currentUiState.copy(error = false) } } + + 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 changesApplied() { + _uiState.update { currentUiState -> + currentUiState.copy(profileLoaded = false) + } + } + + fun madeChanges(): Boolean = + with(uiState.value) { + oldProfile?.locked != privateAccount + || oldProfile?.display_name != name || oldProfile?.note != bio + } + + 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 uploadImage(image: String) { + //TODO fix + val inputStream = + getApplication().contentResolver.openInputStream(image.toUri()) + ?: return + + val size: Long = + if (image.toUri().scheme == "content") { + getApplication().contentResolver.query( + image.toUri(), + 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.toUri().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 inter = api.updateProfilePicture(requestBody.parts[0]) + + postSub = inter + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { it: Account -> + Log.e("qsdfqsdfs", it.toString()) + + }, + { e: Throwable -> + _uiState.update { currentUiState -> + currentUiState.copy( + uploadProgress = 0, + uploadingPicture = true, + error = true + ) + } + e.printStackTrace() + postSub?.dispose() + sub.dispose() + }, + { + _uiState.update { currentUiState -> + currentUiState.copy( + uploadProgress = 100, + uploadingPicture = false + ) + } + postSub?.dispose() + sub.dispose() + } + ) + } } + data class EditProfileActivityUiState( val name: String? = null, val bio: String? = null, val profilePictureUri: Uri?= null, val privateAccount: Boolean? = null, val loadingProfile: Boolean = true, + val profileLoaded: Boolean = false, val sendingProfile: Boolean = false, - val error: Boolean = false + val profileSent: Boolean = false, + val error: Boolean = false, + val uploadingPicture: Boolean = false, + val uploadProgress: Int = 0, ) class EditProfileViewModelFactory(val application: Application) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt index 2c62f4a7..14b39e8c 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt @@ -295,15 +295,21 @@ interface PixelfedAPI { @Header("Authorization") authorization: String? = null ): Account - @Multipart + //@Multipart @PATCH("/api/v1/accounts/update_credentials") suspend fun updateCredentials( - @Part(value = "display_name") displayName: String?, - @Part(value = "note") note: String?, - @Part(value = "locked") locked: Boolean?, + @Query(value = "display_name") displayName: String?, + @Query(value = "note") note: String?, + @Query(value = "locked") locked: Boolean?, // @Part avatar: MultipartBody.Part?, ): Account + @Multipart + @PATCH("/api/v1/accounts/update_credentials") + fun updateProfilePicture( + @Part avatar: MultipartBody.Part? + ): Observable + @GET("/api/v1/accounts/{id}/statuses") suspend fun accountPosts( @Path("id") account_id: String, diff --git a/app/src/main/res/drawable/done.xml b/app/src/main/res/drawable/done.xml index 2728880b..399ee2df 100644 --- a/app/src/main/res/drawable/done.xml +++ b/app/src/main/res/drawable/done.xml @@ -1,5 +1,5 @@ - - + diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_profile.xml index bb0467de..27f23e6c 100644 --- a/app/src/main/res/layout/activity_edit_profile.xml +++ b/app/src/main/res/layout/activity_edit_profile.xml @@ -14,7 +14,9 @@ app:layout_constraintBottom_toTopOf="@+id/textInputLayoutName" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:srcCompat="@tools:sample/avatars" /> + tools:srcCompat="@tools:sample/avatars" + android:contentDescription="@string/profile_picture" /> + @@ -53,17 +55,130 @@ android:id="@+id/bioEditText" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="Add a bio here" /> + android:hint="@string/your_bio" /> - + + + + + + + + + +