start on profile editing functionality

This commit is contained in:
Matthieu 2022-06-23 17:11:11 +02:00
parent 5b3870b80d
commit e35cb17879
13 changed files with 324 additions and 10 deletions

View File

@ -31,7 +31,9 @@
android:name=".posts.AlbumActivity"
android:exported="false"
android:theme="@style/AppTheme.ActionBar.Transparent"/>
<activity
android:name=".profile.EditProfileActivity"
android:exported="false"/>
<activity
android:name=".posts.MediaViewerActivity"
android:configChanges="keyboardHidden|orientation|screenSize"

View File

@ -0,0 +1,71 @@
package org.pixeldroid.app.profile
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.viewModels
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
class EditProfileActivity : BaseActivity() {
private lateinit var model: EditProfileViewModel
private lateinit var binding: ActivityEditProfileBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEditProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.edit_profile)
val _model: EditProfileViewModel by viewModels { EditProfileViewModelFactory(application) }
model = _model
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)
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)
}
}

View File

@ -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<EditProfileActivityUiState> = _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 <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java).newInstance(application)
}
}

View File

@ -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?) {

View File

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

View File

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

View File

@ -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<Field>?,
//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
}
}

View File

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

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/profilePic"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
app:layout_constraintBottom_toTopOf="@+id/textInputLayoutName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayoutName"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/profilePic">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/nameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Your Name"
android:ems="10"
android:imeOptions="actionDone" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayoutBio"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayoutName">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/bioEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Add a bio here" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/savingProgressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editText"

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_apply"
android:orderInCategory="100"
android:title="@string/save"
android:icon="@drawable/done"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -309,4 +309,5 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<item quantity="one">%d reply</item>
<item quantity="other">%d replies</item>
</plurals>
<string name="save">Save</string>
</resources>