Improve consistency of ViewModel and UI

This commit is contained in:
Matthieu 2024-01-23 17:27:45 +01:00
parent 06478cf8a7
commit 1dcf605976
8 changed files with 90 additions and 59 deletions

View File

@ -58,23 +58,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 {

View File

@ -23,6 +23,7 @@ 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.di.PixelfedAPIHolder
import javax.inject.Inject
@ -31,10 +32,13 @@ 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
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
@ -76,15 +80,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,19 +96,16 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
note = bio,
locked = privateAccount,
)
val newAvatarUri = if (profilePictureChanged) {
uploadImage()
profilePictureUri
} else {
account.anyAvatar()?.toUri()
}
oldProfile = account
_uiState.update { currentUiState ->
currentUiState.copy(
bio = account.source?.note
?: account.note?.let { fromHtml(it).toString() },
name = account.display_name,
profilePictureUri = newAvatarUri,
profilePictureUri = if (profilePictureChanged) profilePictureUri
else account.anyAvatar()?.toUri(),
uploadProgress = 0,
uploadingPicture = profilePictureChanged,
privateAccount = account.locked,
sendingProfile = false,
profileSent = true,
@ -118,14 +114,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
)
}
@ -152,12 +147,6 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
}
}
fun changesApplied() {
_uiState.update { currentUiState ->
currentUiState.copy(profileLoaded = false)
}
}
fun madeChanges(): Boolean =
with(uiState.value) {
val privateChanged = oldProfile?.locked != privateAccount
@ -166,6 +155,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
// If source note is null, check note
?: oldProfile?.note?.let { fromHtml(it).toString() != bio }
?: true
profilePictureChanged || privateChanged || displayNameChanged || bioChanged
}
@ -243,20 +233,31 @@ 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.i("ACCOUNT", it.toString())
/* onNext = */ { account: Account ->
account.anyAvatar()?.let {
_uiState.update { currentUiState ->
currentUiState.copy(
profilePictureUri = it.toUri()
)
}
}
},
{ e: Throwable ->
/* onError = */ { e: Throwable ->
_uiState.update { currentUiState ->
currentUiState.copy(
uploadProgress = 0,
uploadingPicture = true,
uploadingPicture = false,
error = true
)
}
@ -264,7 +265,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
postSub?.dispose()
sub.dispose()
},
{
/* onComplete = */ {
_uiState.update { currentUiState ->
currentUiState.copy(
profilePictureChanged = false,

View File

@ -338,21 +338,34 @@ 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 updateProfilePictureMastodon(
@Part avatar: MultipartBody.Part?
): Observable<Account>
@GET("/api/v1/accounts/{id}/statuses")
suspend fun accountPosts(
@Path("id") account_id: String,

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

@ -37,17 +37,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

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