Merge branch 'edit_profile_picture' into 'master'

Fix profile pic edit

Closes #377

See merge request pixeldroid/PixelDroid!569
This commit is contained in:
Matthieu 2024-01-26 12:22:51 +00:00
commit 370aeda4a6
15 changed files with 307 additions and 141 deletions

View File

@ -27,7 +27,7 @@ android {
} }
defaultConfig { defaultConfig {
minSdkVersion 23 minSdkVersion 23
versionCode 26 versionCode 27
targetSdkVersion 34 targetSdkVersion 34
versionName "1.0.beta" + versionCode versionName "1.0.beta" + versionCode

View File

@ -14,6 +14,7 @@ import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -29,6 +30,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
@ -36,7 +38,12 @@ import com.mikepenz.materialdrawer.iconics.iconicsIcon
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.ProfileDrawerItem import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.* import com.mikepenz.materialdrawer.model.interfaces.IProfile
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
import com.mikepenz.materialdrawer.model.interfaces.descriptionText
import com.mikepenz.materialdrawer.model.interfaces.iconUrl
import com.mikepenz.materialdrawer.model.interfaces.nameRes
import com.mikepenz.materialdrawer.model.interfaces.nameText
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.mikepenz.materialdrawer.widget.AccountHeaderView import com.mikepenz.materialdrawer.widget.AccountHeaderView
@ -53,10 +60,10 @@ import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment
import org.pixeldroid.app.settings.SettingsActivity import org.pixeldroid.app.settings.SettingsActivity
import org.pixeldroid.app.utils.BaseActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Notification import org.pixeldroid.app.utils.api.objects.Notification
import org.pixeldroid.app.utils.db.addUser
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.db.updateUserInfoDb
import org.pixeldroid.app.utils.hasInternet import org.pixeldroid.app.utils.hasInternet
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.INSTANCE_NOTIFICATION_TAG import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.INSTANCE_NOTIFICATION_TAG
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG
@ -71,6 +78,8 @@ class MainActivity : BaseActivity() {
private lateinit var header: AccountHeaderView private lateinit var header: AccountHeaderView
private var user: UserDatabaseEntity? = null private var user: UserDatabaseEntity? = null
private lateinit var model: MainActivityViewModel
companion object { companion object {
const val ADD_ACCOUNT_IDENTIFIER: Long = -13 const val ADD_ACCOUNT_IDENTIFIER: Long = -13
} }
@ -102,6 +111,12 @@ class MainActivity : BaseActivity() {
} else { } else {
sendTraceDroidStackTracesIfExist("contact@pixeldroid.org", this) sendTraceDroidStackTracesIfExist("contact@pixeldroid.org", this)
val _model: MainActivityViewModel by viewModels {
MainActivityViewModelFactory(application)
}
model = _model
setupDrawer() setupDrawer()
val tabs: List<() -> Fragment> = listOf( val tabs: List<() -> Fragment> = listOf(
{ {
@ -196,6 +211,7 @@ class MainActivity : BaseActivity() {
Glide.with(this@MainActivity) Glide.with(this@MainActivity)
.load(uri) .load(uri)
.placeholder(placeholder) .placeholder(placeholder)
.circleCrop()
.into(imageView) .into(imageView)
} }
@ -281,16 +297,12 @@ class MainActivity : BaseActivity() {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
try { try {
val domain = user?.instance_uri.orEmpty()
val accessToken = user?.accessToken.orEmpty()
val refreshToken = user?.refreshToken
val clientId = user?.clientId.orEmpty()
val clientSecret = user?.clientSecret.orEmpty()
val api = apiHolder.api ?: apiHolder.setToCurrentUser() val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val account = api.verifyCredentials() val account = api.verifyCredentials()
addUser(db, account, domain, accessToken = accessToken, refreshToken = refreshToken, clientId = clientId, clientSecret = clientSecret) updateUserInfoDb(db, account)
fillDrawerAccountInfo(account.id!!)
//No need to update drawer account info here, the ViewModel listens to db updates
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e("ACCOUNT UPDATE:", exception.toString()) Log.e("ACCOUNT UPDATE:", exception.toString())
} }
@ -337,35 +349,41 @@ class MainActivity : BaseActivity() {
} }
private fun fillDrawerAccountInfo(account: String) { private fun fillDrawerAccountInfo(account: String) {
val users = db.userDao().getAll().toMutableList() lifecycleScope.launch {
users.sortWith { l, r -> lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
when { model.users.collect { list ->
l.isActive && !r.isActive -> -1 val users = list.toMutableList()
r.isActive && !l.isActive -> 1 users.sortWith { l, r ->
else -> 0 when {
l.isActive && !r.isActive -> -1
r.isActive && !l.isActive -> 1
else -> 0
}
}
val profiles: MutableList<IProfile> = users.map { user ->
ProfileDrawerItem().apply {
isSelected = user.isActive
nameText = user.display_name
iconUrl = user.avatar_static
isNameShown = true
identifier = user.user_id.toLong()
descriptionText = user.fullHandle
tag = user.instance_uri
}
}.toMutableList()
// reuse the already existing "add account" item
header.profiles.orEmpty()
.filter { it.identifier == ADD_ACCOUNT_IDENTIFIER }
.take(1)
.forEach { profiles.add(it) }
header.clear()
header.profiles = profiles
header.setActiveProfile(account.toLong())
}
} }
} }
val profiles: MutableList<IProfile> = users.map { user ->
ProfileDrawerItem().apply {
isSelected = user.isActive
nameText = user.display_name
iconUrl = user.avatar_static
isNameShown = true
identifier = user.user_id.toLong()
descriptionText = user.fullHandle
tag = user.instance_uri
}
}.toMutableList()
// reuse the already existing "add account" item
header.profiles.orEmpty()
.filter { it.identifier == ADD_ACCOUNT_IDENTIFIER }
.take(1)
.forEach { profiles.add(it) }
header.clear()
header.profiles = profiles
header.setActiveProfile(account.toLong())
} }
/** /**

View File

@ -0,0 +1,52 @@
package org.pixeldroid.app
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import javax.inject.Inject
class MainActivityViewModel(application: Application) : AndroidViewModel(application) {
@Inject
lateinit var db: AppDatabase
// Mutable state flow that will be used internally in the ViewModel, empty list is given as initial value.
private val _users = MutableStateFlow(emptyList<UserDatabaseEntity>())
// Immutable state flow exposed to UI
val users = _users.asStateFlow()
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
getUsers()
}
private fun getUsers() {
viewModelScope.launch {
db.userDao().getAllFlow().flowOn(Dispatchers.IO)
.collect { users: List<UserDatabaseEntity> ->
_users.update { users }
}
}
}
}
class MainActivityViewModelFactory(
val application: Application,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java).newInstance(application)
}
}

View File

@ -73,6 +73,7 @@ internal fun <T: Any> initAdapter(
swipeRefreshLayout.setOnRefreshListener { swipeRefreshLayout.setOnRefreshListener {
adapter.refresh() adapter.refresh()
adapter.notifyDataSetChanged()
header?.refreshStories() header?.refreshStories()
} }

View File

@ -51,6 +51,7 @@ class EditProfileActivity : BaseActivity() {
}.show() }.show()
} else { } else {
this.isEnabled = false this.isEnabled = false
if (model.submittedChanges) setResult(RESULT_OK)
super.onBackPressedDispatcher.onBackPressed() super.onBackPressedDispatcher.onBackPressed()
} }
} }
@ -58,23 +59,24 @@ class EditProfileActivity : BaseActivity() {
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState -> model.uiState.collect { uiState ->
if(uiState.profileLoaded){ if(binding.bioEditText.text.toString() != uiState.bio) binding.bioEditText.setText(uiState.bio)
binding.bioEditText.setText(uiState.bio) if(binding.nameEditText.text.toString() != uiState.name) binding.nameEditText.setText(uiState.name)
binding.nameEditText.setText(uiState.name)
model.changesApplied() binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.uploadingPicture || uiState.profileSent || uiState.error) View.VISIBLE else View.INVISIBLE
}
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) if(uiState.loadingProfile) binding.progressText.setText(R.string.fetching_profile)
else if(uiState.sendingProfile) binding.progressText.setText(R.string.saving_profile) else if(uiState.sendingProfile) binding.progressText.setText(R.string.saving_profile)
binding.privateSwitch.isChecked = uiState.privateAccount == true binding.privateSwitch.isChecked = uiState.privateAccount == true
Glide.with(binding.profilePic).load(uiState.profilePictureUri) Glide.with(binding.profilePic).load(uiState.profilePictureUri)
.apply(RequestOptions.circleCropTransform()) .apply(RequestOptions.circleCropTransform())
.into(binding.profilePic) .into(binding.profilePic)
binding.savingProgressBar.visibility = if(uiState.error || uiState.profileSent) View.GONE binding.savingProgressBar.visibility =
else View.VISIBLE if(uiState.error || (uiState.profileSent && !uiState.uploadingPicture)) View.GONE
else View.VISIBLE
if(uiState.profileSent){ if(uiState.profileSent && !uiState.uploadingPicture && !uiState.error){
binding.progressText.setText(R.string.profile_saved) binding.progressText.setText(R.string.profile_saved)
binding.done.visibility = View.VISIBLE binding.done.visibility = View.VISIBLE
} else { } else {
@ -112,18 +114,18 @@ class EditProfileActivity : BaseActivity() {
} }
} }
// binding.changeImageButton.setOnClickListener { binding.profilePic.setOnClickListener {
// Intent(Intent.ACTION_GET_CONTENT).apply { Intent(Intent.ACTION_GET_CONTENT).apply {
// type = "*/*" type = "*/*"
// putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*")) putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*"))
// action = Intent.ACTION_GET_CONTENT action = Intent.ACTION_GET_CONTENT
// addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
// putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
// uploadImageResultContract.launch( uploadImageResultContract.launch(
// Intent.createChooser(this, null) Intent.createChooser(this, null)
// ) )
// } }
// } }
} }
private val uploadImageResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val uploadImageResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@ -137,10 +139,10 @@ class EditProfileActivity : BaseActivity() {
val imageUri: String = clipData.getItemAt(i).uri.toString() val imageUri: String = clipData.getItemAt(i).uri.toString()
images.add(imageUri) images.add(imageUri)
} }
model.uploadImage(images.first()) model.updateImage(images.first())
} else if (data.data != null) { } else if (data.data != null) {
images.add(data.data!!.toString()) images.add(data.data!!.toString())
model.uploadImage(images.first()) model.updateImage(images.first())
} }
} }
} }

View File

@ -23,7 +23,10 @@ import org.pixeldroid.app.postCreation.ProgressRequestBody
import org.pixeldroid.app.posts.fromHtml import org.pixeldroid.app.posts.fromHtml
import org.pixeldroid.app.utils.PixelDroidApplication import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.api.objects.Account 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 org.pixeldroid.app.utils.di.PixelfedAPIHolder
import retrofit2.HttpException
import javax.inject.Inject import javax.inject.Inject
class EditProfileViewModel(application: Application) : AndroidViewModel(application) { class EditProfileViewModel(application: Application) : AndroidViewModel(application) {
@ -31,10 +34,16 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
@Inject @Inject
lateinit var apiHolder: PixelfedAPIHolder lateinit var apiHolder: PixelfedAPIHolder
@Inject
lateinit var db: AppDatabase
private val _uiState = MutableStateFlow(EditProfileActivityUiState()) private val _uiState = MutableStateFlow(EditProfileActivityUiState())
val uiState: StateFlow<EditProfileActivityUiState> = _uiState val uiState: StateFlow<EditProfileActivityUiState> = _uiState
var oldProfile: Account? = null private var oldProfile: Account? = null
var submittedChanges = false
private set
init { init {
(application as PixelDroidApplication).getAppComponent().inject(this) (application as PixelDroidApplication).getAppComponent().inject(this)
@ -46,6 +55,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
val api = apiHolder.api ?: apiHolder.setToCurrentUser() val api = apiHolder.api ?: apiHolder.setToCurrentUser()
try { try {
val profile = api.verifyCredentials() val profile = api.verifyCredentials()
updateUserInfoDb(db, profile)
if (oldProfile == null) oldProfile = profile if (oldProfile == null) oldProfile = profile
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
@ -76,15 +86,10 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
fun sendProfile() { fun sendProfile() {
val api = apiHolder.api ?: apiHolder.setToCurrentUser() val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val requestBody =
null //MultipartBody.Part.createFormData("avatar", System.currentTimeMillis().toString(), avatarBody)
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
sendingProfile = true, sendingProfile = true,
profileSent = false, profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = false error = false
) )
} }
@ -97,12 +102,17 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
note = bio, note = bio,
locked = privateAccount, locked = privateAccount,
) )
if (madeChanges()) submittedChanges = true
oldProfile = account oldProfile = account
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
bio = account.source?.note ?: account.note?.let {fromHtml(it).toString()}, bio = account.source?.note
?: account.note?.let { fromHtml(it).toString() },
name = account.display_name, name = account.display_name,
profilePictureUri = account.anyAvatar()?.toUri(), profilePictureUri = if (profilePictureChanged) profilePictureUri
else account.anyAvatar()?.toUri(),
uploadProgress = 0,
uploadingPicture = profilePictureChanged,
privateAccount = account.locked, privateAccount = account.locked,
sendingProfile = false, sendingProfile = false,
profileSent = true, profileSent = true,
@ -111,14 +121,13 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
error = false error = false
) )
} }
if(profilePictureChanged) uploadImage()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e("TAG", exception.toString()) Log.e("TAG", exception.toString())
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
sendingProfile = false, sendingProfile = false,
profileSent = false, profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = true error = true
) )
} }
@ -145,20 +154,16 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
} }
} }
fun changesApplied() {
_uiState.update { currentUiState ->
currentUiState.copy(profileLoaded = false)
}
}
fun madeChanges(): Boolean = fun madeChanges(): Boolean =
with(uiState.value) { with(uiState.value) {
val bioUnchanged: Boolean = oldProfile?.source?.note?.let { it != bio } val privateChanged = oldProfile?.locked != privateAccount
// If source note is null, check note 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 } ?: oldProfile?.note?.let { fromHtml(it).toString() != bio }
?: true ?: true
oldProfile?.locked != privateAccount || oldProfile?.display_name != name
|| bioUnchanged profilePictureChanged || privateChanged || displayNameChanged || bioChanged
} }
fun clickedCard() { fun clickedCard() {
@ -178,16 +183,27 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
} }
} }
fun uploadImage(image: String) { fun updateImage(image: String) {
//TODO fix _uiState.update { currentUiState ->
currentUiState.copy(
profilePictureUri = image.toUri(),
profilePictureChanged = true,
profileSent = false
)
}
}
private fun uploadImage() {
val image = uiState.value.profilePictureUri!!
val inputStream = val inputStream =
getApplication<PixelDroidApplication>().contentResolver.openInputStream(image.toUri()) getApplication<PixelDroidApplication>().contentResolver.openInputStream(image)
?: return ?: return
val size: Long = val size: Long =
if (image.toUri().scheme == "content") { if (image.scheme == "content") {
getApplication<PixelDroidApplication>().contentResolver.query( getApplication<PixelDroidApplication>().contentResolver.query(
image.toUri(), image,
null, null,
null, null,
null, null,
@ -203,7 +219,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
cursor.getLong(sizeIndex) cursor.getLong(sizeIndex)
} ?: 0 } ?: 0
} else { } else {
image.toUri().toFile().length() image.toFile().length()
} }
val imagePart = ProgressRequestBody(inputStream, size, "image/*") val imagePart = ProgressRequestBody(inputStream, size, "image/*")
@ -225,21 +241,32 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
var postSub: Disposable? = null var postSub: Disposable? = null
val api = apiHolder.api ?: apiHolder.setToCurrentUser() val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val inter = api.updateProfilePicture(requestBody.parts[0])
val pixelfed = db.instanceDao().getActiveInstance().pixelfed
val inter =
if(pixelfed) api.updateProfilePicture(requestBody.parts[0])
else api.updateProfilePictureMastodon(requestBody.parts[0])
postSub = inter postSub = inter
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
{ it: Account -> /* onNext = */ { account: Account ->
Log.e("qsdfqsdfs", it.toString()) account.anyAvatar()?.let {
_uiState.update { currentUiState ->
currentUiState.copy(
profilePictureUri = it.toUri()
)
}
}
}, },
{ e: Throwable -> /* onError = */ { e: Throwable ->
Log.e("error", (e as? HttpException)?.message().orEmpty())
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
uploadProgress = 0, uploadProgress = 0,
uploadingPicture = true, uploadingPicture = false,
error = true error = true
) )
} }
@ -247,9 +274,10 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
postSub?.dispose() postSub?.dispose()
sub.dispose() sub.dispose()
}, },
{ /* onComplete = */ {
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
profilePictureChanged = false,
uploadProgress = 100, uploadProgress = 100,
uploadingPicture = false uploadingPicture = false
) )
@ -265,7 +293,8 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
data class EditProfileActivityUiState( data class EditProfileActivityUiState(
val name: String? = null, val name: String? = null,
val bio: String? = null, val bio: String? = null,
val profilePictureUri: Uri?= null, val profilePictureUri: Uri? = null,
val profilePictureChanged: Boolean = false,
val privateAccount: Boolean? = null, val privateAccount: Boolean? = null,
val loadingProfile: Boolean = true, val loadingProfile: Boolean = true,
val profileLoaded: Boolean = false, val profileLoaded: Boolean = false,

View File

@ -6,6 +6,7 @@ import android.text.method.LinkMovementMethod
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -16,11 +17,14 @@ import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityProfileBinding import org.pixeldroid.app.databinding.ActivityProfileBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedFeedFragment
import org.pixeldroid.app.posts.parseHTMLText import org.pixeldroid.app.posts.parseHTMLText
import org.pixeldroid.app.utils.BaseActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.PixelfedAPI import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.FeedContent
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.db.updateUserInfoDb
import org.pixeldroid.app.utils.setProfileImageFromURL import org.pixeldroid.app.utils.setProfileImageFromURL
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
@ -56,7 +60,7 @@ class ProfileActivity : BaseActivity() {
setContent(account) setContent(account)
} }
private fun createProfileTabs(account: Account?): Array<Fragment>{ private fun createProfileTabs(account: Account?): Array<UncachedFeedFragment<FeedContent>> {
val profileFeedFragment = ProfileFeedFragment() val profileFeedFragment = ProfileFeedFragment()
profileFeedFragment.arguments = Bundle().apply { profileFeedFragment.arguments = Bundle().apply {
@ -80,7 +84,7 @@ class ProfileActivity : BaseActivity() {
putSerializable(ProfileFeedFragment.COLLECTIONS, true) putSerializable(ProfileFeedFragment.COLLECTIONS, true)
} }
val returnArray: Array<Fragment> = arrayOf( val returnArray: Array<UncachedFeedFragment<FeedContent>> = arrayOf(
profileGridFragment, profileGridFragment,
profileFeedFragment, profileFeedFragment,
profileCollectionsFragment profileCollectionsFragment
@ -100,7 +104,7 @@ class ProfileActivity : BaseActivity() {
} }
private fun setupTabs( private fun setupTabs(
tabs: Array<Fragment> tabs: Array<UncachedFeedFragment<FeedContent>>
){ ){
binding.viewPager.adapter = object : FragmentStateAdapter(this) { binding.viewPager.adapter = object : FragmentStateAdapter(this) {
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
@ -134,7 +138,6 @@ class ProfileActivity : BaseActivity() {
}.attach() }.attach()
} }
private fun setContent(account: Account?) { private fun setContent(account: Account?) {
if(account != null) { if(account != null) {
setViews(account) setViews(account)
@ -152,6 +155,9 @@ class ProfileActivity : BaseActivity() {
).show() ).show()
return@launchWhenResumed return@launchWhenResumed
} }
updateUserInfoDb(db, myAccount)
setViews(myAccount) setViews(myAccount)
} }
} }
@ -217,9 +223,15 @@ class ProfileActivity : BaseActivity() {
) )
} }
private val editResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
// Profile was edited, reload
setContent(null)
}
}
private fun onClickEditButton() { private fun onClickEditButton() {
val intent = Intent(this, EditProfileActivity::class.java) editResult.launch(Intent(this, EditProfileActivity::class.java))
ContextCompat.startActivity(this, intent, null)
} }
private fun onClickFollowers(account: Account?) { private fun onClickFollowers(account: Account?) {

View File

@ -338,18 +338,31 @@ interface PixelfedAPI {
@Header("Authorization") authorization: String? = null @Header("Authorization") authorization: String? = null
): Account ): Account
//@Multipart
@PATCH("/api/v1/accounts/update_credentials") @PATCH("/api/v1/accounts/update_credentials")
suspend fun updateCredentials( suspend fun updateCredentials(
@Query(value = "display_name") displayName: String?, @Query(value = "display_name") displayName: String?,
@Query(value = "note") note: String?, @Query(value = "note") note: String?,
@Query(value = "locked") locked: Boolean?, @Query(value = "locked") locked: Boolean?,
// @Part avatar: MultipartBody.Part?,
): Account ): Account
/**
* Pixelfed uses PHP, multipart uploads don't work through PATCH so we use POST as suggested
* here: https://github.com/pixelfed/pixelfed/issues/4250
* However, changing to POST breaks the upload on Mastodon.
*
* To have this work on Pixelfed and Mastodon without special logic to distinguish the two,
* we'll have to wait for PHP 8.4 and https://wiki.php.net/rfc/rfc1867-non-post
* which should come out end of 2024
*/
@Multipart
@POST("/api/v1/accounts/update_credentials")
fun updateProfilePicture(
@Part avatar: MultipartBody.Part?
): Observable<Account>
@Multipart @Multipart
@PATCH("/api/v1/accounts/update_credentials") @PATCH("/api/v1/accounts/update_credentials")
fun updateProfilePicture( fun updateProfilePictureMastodon(
@Part avatar: MultipartBody.Part? @Part avatar: MultipartBody.Part?
): Observable<Account> ): Observable<Account>

View File

@ -22,7 +22,7 @@ import org.pixeldroid.app.utils.api.objects.Notification
PublicFeedStatusDatabaseEntity::class, PublicFeedStatusDatabaseEntity::class,
Notification::class Notification::class
], ],
version = 5 version = 6
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -44,4 +44,9 @@ val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE instances ADD COLUMN videoEnabled INTEGER NOT NULL DEFAULT 1") database.execSQL("ALTER TABLE instances ADD COLUMN videoEnabled INTEGER NOT NULL DEFAULT 1")
} }
}
val MIGRATION_5_6 = object : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE instances ADD COLUMN pixelfed INTEGER NOT NULL DEFAULT 1")
}
} }

View File

@ -13,21 +13,34 @@ import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEF
import org.pixeldroid.app.utils.normalizeDomain import org.pixeldroid.app.utils.normalizeDomain
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true, suspend fun addUser(
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String) { db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true,
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String,
) {
db.userDao().insertOrUpdate( db.userDao().insertOrUpdate(
UserDatabaseEntity( UserDatabaseEntity(
user_id = account.id!!, user_id = account.id!!,
instance_uri = normalizeDomain(instance_uri), instance_uri = normalizeDomain(instance_uri),
username = account.username!!, username = account.username!!,
display_name = account.getDisplayName(), display_name = account.getDisplayName(),
avatar_static = account.anyAvatar().orEmpty(), avatar_static = account.anyAvatar().orEmpty(),
isActive = activeUser, isActive = activeUser,
accessToken = accessToken, accessToken = accessToken,
refreshToken = refreshToken, refreshToken = refreshToken,
clientId = clientId, clientId = clientId,
clientSecret = clientSecret clientSecret = clientSecret
) )
)
}
suspend fun updateUserInfoDb(db: AppDatabase, account: Account) {
val user = db.userDao().getActiveUser()!!
db.userDao().updateUserAccountDetails(
account.username.orEmpty(),
account.display_name.orEmpty(),
account.anyAvatar().orEmpty(),
user.user_id,
user.instance_uri
) )
} }
@ -37,17 +50,21 @@ fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = nu
uri = normalizeDomain(metadata?.config?.site?.url!!), uri = normalizeDomain(metadata?.config?.site?.url!!),
title = metadata.config.site.name!!, title = metadata.config.site.name!!,
maxStatusChars = metadata.config.uploader?.max_caption_length!!.toInt(), maxStatusChars = metadata.config.uploader?.max_caption_length!!.toInt(),
maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_PHOTO_SIZE, maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull()
?: DEFAULT_MAX_PHOTO_SIZE,
// Pixelfed doesn't distinguish between max photo and video size // Pixelfed doesn't distinguish between max photo and video size
maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_VIDEO_SIZE, maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull()
?: DEFAULT_MAX_VIDEO_SIZE,
albumLimit = metadata.config.uploader.album_limit?.toIntOrNull() ?: DEFAULT_ALBUM_LIMIT, albumLimit = metadata.config.uploader.album_limit?.toIntOrNull() ?: DEFAULT_ALBUM_LIMIT,
videoEnabled = metadata.config.features?.video ?: DEFAULT_VIDEO_ENABLED videoEnabled = metadata.config.features?.video ?: DEFAULT_VIDEO_ENABLED,
pixelfed = metadata.software?.repo?.contains("pixelfed", ignoreCase = true) == true
) )
} ?: instance?.run { } ?: instance?.run {
InstanceDatabaseEntity( InstanceDatabaseEntity(
uri = normalizeDomain(uri.orEmpty()), uri = normalizeDomain(uri.orEmpty()),
title = title.orEmpty(), title = title.orEmpty(),
maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS, maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS,
pixelfed = false
) )
} ?: throw IllegalArgumentException("Cannot store instance where both are null") } ?: throw IllegalArgumentException("Cannot store instance where both are null")

View File

@ -11,6 +11,10 @@ interface InstanceDao {
@Query("SELECT * FROM instances WHERE uri=:instanceUri") @Query("SELECT * FROM instances WHERE uri=:instanceUri")
fun getInstance(instanceUri: String): InstanceDatabaseEntity fun getInstance(instanceUri: String): InstanceDatabaseEntity
@Query("SELECT * FROM instances WHERE uri=(SELECT users.instance_uri FROM users WHERE isActive=1)")
fun getActiveInstance(): InstanceDatabaseEntity
/** /**
* Insert an instance, if it already exists return -1 * Insert an instance, if it already exists return -1
*/ */

View File

@ -1,6 +1,7 @@
package org.pixeldroid.app.utils.db.dao package org.pixeldroid.app.utils.db.dao
import androidx.room.* import androidx.room.*
import kotlinx.coroutines.flow.Flow
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
@Dao @Dao
@ -9,17 +10,21 @@ interface UserDao {
* Insert a user, if it already exists return -1 * Insert a user, if it already exists return -1
*/ */
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertUser(user: UserDatabaseEntity): Long suspend fun insertUser(user: UserDatabaseEntity): Long
@Transaction @Transaction
fun insertOrUpdate(user: UserDatabaseEntity) { suspend fun insertOrUpdate(user: UserDatabaseEntity) {
if (insertUser(user) == -1L) { if (insertUser(user) == -1L) {
updateUser(user) updateUser(user)
} }
} }
@Update @Update
fun updateUser(user: UserDatabaseEntity) suspend fun updateUser(user: UserDatabaseEntity)
@Query("UPDATE users SET username = :username, display_name = :displayName, avatar_static = :avatarStatic WHERE user_id = :id and instance_uri = :instanceUri")
suspend fun updateUserAccountDetails(username: String, displayName: String, avatarStatic: String, id: String, instanceUri: String)
@Query("UPDATE users SET accessToken = :accessToken, refreshToken = :refreshToken WHERE user_id = :id and instance_uri = :instanceUri") @Query("UPDATE users SET accessToken = :accessToken, refreshToken = :refreshToken WHERE user_id = :id and instance_uri = :instanceUri")
fun updateAccessToken(accessToken: String, refreshToken: String, id: String, instanceUri: String) fun updateAccessToken(accessToken: String, refreshToken: String, id: String, instanceUri: String)
@ -27,6 +32,9 @@ interface UserDao {
@Query("SELECT * FROM users") @Query("SELECT * FROM users")
fun getAll(): List<UserDatabaseEntity> fun getAll(): List<UserDatabaseEntity>
@Query("SELECT * FROM users")
fun getAllFlow(): Flow<List<UserDatabaseEntity>>
@Query("SELECT * FROM users WHERE isActive=1") @Query("SELECT * FROM users WHERE isActive=1")
fun getActiveUser(): UserDatabaseEntity? fun getActiveUser(): UserDatabaseEntity?

View File

@ -4,20 +4,22 @@ import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@Entity(tableName = "instances") @Entity(tableName = "instances")
data class InstanceDatabaseEntity ( data class InstanceDatabaseEntity(
@PrimaryKey var uri: String, @PrimaryKey var uri: String,
var title: String, var title: String,
var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS, var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS,
// Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB // Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB
var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE, var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE,
// Mastodon has different file limits for videos, default of 40MB // Mastodon has different file limits for videos, default of 40MB
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE, var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4 // How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
var albumLimit: Int = DEFAULT_ALBUM_LIMIT, var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
// Is video functionality enabled on this instance? // Is video functionality enabled on this instance?
var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED, var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED,
// Is this Pixelfed instance?
var pixelfed: Boolean = true,
) { ) {
companion object{ companion object {
// Default max number of chars for Mastodon: used when their is no other value supplied by // Default max number of chars for Mastodon: used when their is no other value supplied by
// either NodeInfo or the instance endpoint // either NodeInfo or the instance endpoint
const val DEFAULT_MAX_TOOT_CHARS = 500 const val DEFAULT_MAX_TOOT_CHARS = 500

View File

@ -7,6 +7,7 @@ import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.BaseFragment
import dagger.Component import dagger.Component
import org.pixeldroid.app.MainActivityViewModel
import org.pixeldroid.app.postCreation.PostCreationViewModel import org.pixeldroid.app.postCreation.PostCreationViewModel
import org.pixeldroid.app.profile.EditProfileViewModel import org.pixeldroid.app.profile.EditProfileViewModel
import org.pixeldroid.app.stories.StoriesViewModel import org.pixeldroid.app.stories.StoriesViewModel
@ -25,6 +26,7 @@ interface ApplicationComponent {
fun inject(postCreationViewModel: PostCreationViewModel) fun inject(postCreationViewModel: PostCreationViewModel)
fun inject(editProfileViewModel: EditProfileViewModel) fun inject(editProfileViewModel: EditProfileViewModel)
fun inject(storiesViewModel: StoriesViewModel) fun inject(storiesViewModel: StoriesViewModel)
fun inject(mainActivityViewModel: MainActivityViewModel)
val context: Context? val context: Context?
val application: Application? val application: Application?

View File

@ -7,6 +7,7 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import org.pixeldroid.app.utils.db.MIGRATION_3_4 import org.pixeldroid.app.utils.db.MIGRATION_3_4
import org.pixeldroid.app.utils.db.MIGRATION_4_5 import org.pixeldroid.app.utils.db.MIGRATION_4_5
import org.pixeldroid.app.utils.db.MIGRATION_5_6
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -18,7 +19,7 @@ class DatabaseModule(private val context: Context) {
return Room.databaseBuilder( return Room.databaseBuilder(
context, context,
AppDatabase::class.java, "pixeldroid" AppDatabase::class.java, "pixeldroid"
).addMigrations(MIGRATION_3_4).addMigrations(MIGRATION_4_5) ).addMigrations(MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6)
.allowMainThreadQueries().build() .allowMainThreadQueries().build()
} }
} }