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 {
minSdkVersion 23
versionCode 26
versionCode 27
targetSdkVersion 34
versionName "1.0.beta" + versionCode

View File

@ -14,6 +14,7 @@ import android.view.View
import android.widget.ImageView
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
@ -29,6 +30,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.color.DynamicColors
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.ProfileDrawerItem
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.DrawerImageLoader
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.utils.BaseActivity
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.PublicFeedStatusDatabaseEntity
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.notificationsWorker.NotificationsWorker.Companion.INSTANCE_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 var user: UserDatabaseEntity? = null
private lateinit var model: MainActivityViewModel
companion object {
const val ADD_ACCOUNT_IDENTIFIER: Long = -13
}
@ -102,6 +111,12 @@ class MainActivity : BaseActivity() {
} else {
sendTraceDroidStackTracesIfExist("contact@pixeldroid.org", this)
val _model: MainActivityViewModel by viewModels {
MainActivityViewModelFactory(application)
}
model = _model
setupDrawer()
val tabs: List<() -> Fragment> = listOf(
{
@ -196,6 +211,7 @@ class MainActivity : BaseActivity() {
Glide.with(this@MainActivity)
.load(uri)
.placeholder(placeholder)
.circleCrop()
.into(imageView)
}
@ -281,16 +297,12 @@ class MainActivity : BaseActivity() {
lifecycleScope.launchWhenCreated {
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 account = api.verifyCredentials()
addUser(db, account, domain, accessToken = accessToken, refreshToken = refreshToken, clientId = clientId, clientSecret = clientSecret)
fillDrawerAccountInfo(account.id!!)
updateUserInfoDb(db, account)
//No need to update drawer account info here, the ViewModel listens to db updates
} catch (exception: Exception) {
Log.e("ACCOUNT UPDATE:", exception.toString())
}
@ -337,35 +349,41 @@ class MainActivity : BaseActivity() {
}
private fun fillDrawerAccountInfo(account: String) {
val users = db.userDao().getAll().toMutableList()
users.sortWith { l, r ->
when {
l.isActive && !r.isActive -> -1
r.isActive && !l.isActive -> 1
else -> 0
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
model.users.collect { list ->
val users = list.toMutableList()
users.sortWith { l, r ->
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 {
adapter.refresh()
adapter.notifyDataSetChanged()
header?.refreshStories()
}

View File

@ -51,6 +51,7 @@ class EditProfileActivity : BaseActivity() {
}.show()
} else {
this.isEnabled = false
if (model.submittedChanges) setResult(RESULT_OK)
super.onBackPressedDispatcher.onBackPressed()
}
}
@ -58,23 +59,24 @@ class EditProfileActivity : BaseActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
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(binding.bioEditText.text.toString() != uiState.bio) binding.bioEditText.setText(uiState.bio)
if(binding.nameEditText.text.toString() != uiState.name) binding.nameEditText.setText(uiState.name)
binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.uploadingPicture || 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
binding.savingProgressBar.visibility =
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.done.visibility = View.VISIBLE
} else {
@ -112,18 +114,18 @@ class EditProfileActivity : BaseActivity() {
}
}
// 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)
// )
// }
// }
binding.profilePic.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 ->
@ -137,10 +139,10 @@ class EditProfileActivity : BaseActivity() {
val imageUri: String = clipData.getItemAt(i).uri.toString()
images.add(imageUri)
}
model.uploadImage(images.first())
model.updateImage(images.first())
} else if (data.data != null) {
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.utils.PixelDroidApplication
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.updateUserInfoDb
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import retrofit2.HttpException
import javax.inject.Inject
class EditProfileViewModel(application: Application) : AndroidViewModel(application) {
@ -31,10 +34,16 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
@Inject
lateinit var apiHolder: PixelfedAPIHolder
@Inject
lateinit var db: AppDatabase
private val _uiState = MutableStateFlow(EditProfileActivityUiState())
val uiState: StateFlow<EditProfileActivityUiState> = _uiState
var oldProfile: Account? = null
private var oldProfile: Account? = null
var submittedChanges = false
private set
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
@ -46,6 +55,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
try {
val profile = api.verifyCredentials()
updateUserInfoDb(db, profile)
if (oldProfile == null) oldProfile = profile
_uiState.update { currentUiState ->
currentUiState.copy(
@ -76,15 +86,10 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
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
)
}
@ -97,12 +102,17 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
note = bio,
locked = privateAccount,
)
if (madeChanges()) submittedChanges = true
oldProfile = account
_uiState.update { currentUiState ->
currentUiState.copy(
bio = account.source?.note ?: account.note?.let {fromHtml(it).toString()},
bio = account.source?.note
?: account.note?.let { fromHtml(it).toString() },
name = account.display_name,
profilePictureUri = account.anyAvatar()?.toUri(),
profilePictureUri = if (profilePictureChanged) profilePictureUri
else account.anyAvatar()?.toUri(),
uploadProgress = 0,
uploadingPicture = profilePictureChanged,
privateAccount = account.locked,
sendingProfile = false,
profileSent = true,
@ -111,14 +121,13 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
error = false
)
}
if(profilePictureChanged) uploadImage()
} catch (exception: Exception) {
Log.e("TAG", exception.toString())
_uiState.update { currentUiState ->
currentUiState.copy(
sendingProfile = false,
profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = true
)
}
@ -145,20 +154,16 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
}
}
fun changesApplied() {
_uiState.update { currentUiState ->
currentUiState.copy(profileLoaded = false)
}
}
fun madeChanges(): Boolean =
with(uiState.value) {
val bioUnchanged: Boolean = oldProfile?.source?.note?.let { it != bio }
// If source note is null, check note
val privateChanged = oldProfile?.locked != privateAccount
val displayNameChanged = oldProfile?.display_name != name
val bioChanged: Boolean = oldProfile?.source?.note?.let { it != bio }
// If source note is null, check note
?: oldProfile?.note?.let { fromHtml(it).toString() != bio }
?: true
oldProfile?.locked != privateAccount || oldProfile?.display_name != name
|| bioUnchanged
profilePictureChanged || privateChanged || displayNameChanged || bioChanged
}
fun clickedCard() {
@ -178,16 +183,27 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
}
}
fun uploadImage(image: String) {
//TODO fix
fun updateImage(image: String) {
_uiState.update { currentUiState ->
currentUiState.copy(
profilePictureUri = image.toUri(),
profilePictureChanged = true,
profileSent = false
)
}
}
private fun uploadImage() {
val image = uiState.value.profilePictureUri!!
val inputStream =
getApplication<PixelDroidApplication>().contentResolver.openInputStream(image.toUri())
getApplication<PixelDroidApplication>().contentResolver.openInputStream(image)
?: return
val size: Long =
if (image.toUri().scheme == "content") {
if (image.scheme == "content") {
getApplication<PixelDroidApplication>().contentResolver.query(
image.toUri(),
image,
null,
null,
null,
@ -203,7 +219,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
cursor.getLong(sizeIndex)
} ?: 0
} else {
image.toUri().toFile().length()
image.toFile().length()
}
val imagePart = ProgressRequestBody(inputStream, size, "image/*")
@ -225,21 +241,32 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
var postSub: Disposable? = null
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
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ it: Account ->
Log.e("qsdfqsdfs", it.toString())
/* onNext = */ { account: Account ->
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 ->
currentUiState.copy(
uploadProgress = 0,
uploadingPicture = true,
uploadingPicture = false,
error = true
)
}
@ -247,9 +274,10 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
postSub?.dispose()
sub.dispose()
},
{
/* onComplete = */ {
_uiState.update { currentUiState ->
currentUiState.copy(
profilePictureChanged = false,
uploadProgress = 100,
uploadingPicture = false
)
@ -265,7 +293,8 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
data class EditProfileActivityUiState(
val name: String? = null,
val bio: String? = null,
val profilePictureUri: Uri?= null,
val profilePictureUri: Uri? = null,
val profilePictureChanged: Boolean = false,
val privateAccount: Boolean? = null,
val loadingProfile: Boolean = true,
val profileLoaded: Boolean = false,

View File

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

View File

@ -338,18 +338,31 @@ 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
/**
* 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
@PATCH("/api/v1/accounts/update_credentials")
fun updateProfilePicture(
fun updateProfilePictureMastodon(
@Part avatar: MultipartBody.Part?
): Observable<Account>

View File

@ -22,7 +22,7 @@ import org.pixeldroid.app.utils.api.objects.Notification
PublicFeedStatusDatabaseEntity::class,
Notification::class
],
version = 5
version = 6
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
@ -44,4 +44,9 @@ val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
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 java.lang.IllegalArgumentException
fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true,
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String) {
suspend fun addUser(
db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true,
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String,
) {
db.userDao().insertOrUpdate(
UserDatabaseEntity(
user_id = account.id!!,
instance_uri = normalizeDomain(instance_uri),
username = account.username!!,
display_name = account.getDisplayName(),
avatar_static = account.anyAvatar().orEmpty(),
isActive = activeUser,
accessToken = accessToken,
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
)
UserDatabaseEntity(
user_id = account.id!!,
instance_uri = normalizeDomain(instance_uri),
username = account.username!!,
display_name = account.getDisplayName(),
avatar_static = account.anyAvatar().orEmpty(),
isActive = activeUser,
accessToken = accessToken,
refreshToken = refreshToken,
clientId = clientId,
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!!),
title = metadata.config.site.name!!,
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
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,
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 {
InstanceDatabaseEntity(
uri = normalizeDomain(uri.orEmpty()),
title = title.orEmpty(),
maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS,
uri = normalizeDomain(uri.orEmpty()),
title = title.orEmpty(),
maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS,
pixelfed = false
)
} ?: throw IllegalArgumentException("Cannot store instance where both are null")

View File

@ -11,6 +11,10 @@ interface InstanceDao {
@Query("SELECT * FROM instances WHERE uri=:instanceUri")
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
*/

View File

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

View File

@ -4,20 +4,22 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "instances")
data class InstanceDatabaseEntity (
@PrimaryKey var uri: String,
var title: String,
var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS,
// Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB
var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE,
// Mastodon has different file limits for videos, default of 40MB
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
// Is video functionality enabled on this instance?
var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED,
data class InstanceDatabaseEntity(
@PrimaryKey var uri: String,
var title: String,
var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS,
// Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB
var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE,
// Mastodon has different file limits for videos, default of 40MB
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
// Is video functionality enabled on this instance?
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
// either NodeInfo or the instance endpoint
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.BaseFragment
import dagger.Component
import org.pixeldroid.app.MainActivityViewModel
import org.pixeldroid.app.postCreation.PostCreationViewModel
import org.pixeldroid.app.profile.EditProfileViewModel
import org.pixeldroid.app.stories.StoriesViewModel
@ -25,6 +26,7 @@ interface ApplicationComponent {
fun inject(postCreationViewModel: PostCreationViewModel)
fun inject(editProfileViewModel: EditProfileViewModel)
fun inject(storiesViewModel: StoriesViewModel)
fun inject(mainActivityViewModel: MainActivityViewModel)
val context: Context?
val application: Application?

View File

@ -7,6 +7,7 @@ import dagger.Module
import dagger.Provides
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_5_6
import javax.inject.Singleton
@Module
@ -18,7 +19,7 @@ class DatabaseModule(private val context: Context) {
return Room.databaseBuilder(
context,
AppDatabase::class.java, "pixeldroid"
).addMigrations(MIGRATION_3_4).addMigrations(MIGRATION_4_5)
).addMigrations(MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6)
.allowMainThreadQueries().build()
}
}