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/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"/>
-
+
+ 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)
+
+ 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 {
+ menuInflater.inflate(R.menu.edit_profile_menu, menu)
+ 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.sendProfile()
+ 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..f0613ec5
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt
@@ -0,0 +1,319 @@
+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
+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) {
+
+ @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 {
+ val profile = api.verifyCredentials()
+ 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: 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 sendProfile() {
+ val api = apiHolder.api ?: apiHolder.setToCurrentUser()
+
+ 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 {
+ val account = api.updateCredentials(
+ displayName = name,
+ note = bio,
+ locked = privateAccount,
+ )
+ 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(
+ sendingProfile = false,
+ profileSent = false,
+ loadingProfile = false,
+ profileLoaded = false,
+ error = true
+ )
+ }
+ } catch (exception: HttpException) {
+ Log.e("TAG", exception.toString())
+ _uiState.update { currentUiState ->
+ currentUiState.copy(
+ sendingProfile = false,
+ profileSent = false,
+ loadingProfile = false,
+ profileLoaded = false,
+ error = true
+ )
+ }
+ } catch (exception: Exception) {
+ Log.e("TAG", exception.toString())
+
+ }
+ }
+ }
+ }
+
+ fun errorShown() {
+ _uiState.update { currentUiState ->
+ 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 profileSent: Boolean = false,
+ val error: Boolean = false,
+ val uploadingPicture: Boolean = false,
+ val uploadProgress: Int = 0,
+)
+
+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..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,6 +295,20 @@ interface PixelfedAPI {
@Header("Authorization") authorization: String? = null
): Account
+ //@Multipart
+ @PATCH("/api/v1/accounts/update_credentials")
+ suspend fun updateCredentials(
+ @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(
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..399ee2df
--- /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..27f23e6c
--- /dev/null
+++ b/app/src/main/res/layout/activity_edit_profile.xml
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..d5dfbe29 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -309,4 +309,17 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"
- %d reply
- %d replies
+ Save
+ Use dynamic color from your system
+ More profile settings
+ Private Account
+ When your account is private, only people you approve can see your photos and videos on pixelfed. Your existing followers won\'t be affected.
+ Your bio
+ Your Name
+ You did not save your changes. Exit?
+ Fetching your profile...
+ Saving your profile
+ Changes saved!
+ Something went wrong. Tap to retry
+ Change your profile picture
\ No newline at end of file
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 7da4b397..54bc404e 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -8331,6 +8331,14 @@
+
+
+
+
+
+
+
+
@@ -8756,6 +8764,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+