diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index 2ccf27823..e0f6a3bc8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -30,7 +30,6 @@ import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged -import androidx.lifecycle.LiveData import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.bumptech.glide.Glide @@ -59,6 +58,7 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch class EditProfileActivity : BaseActivity(), Injectable { @@ -152,51 +152,54 @@ class EditProfileActivity : BaseActivity(), Injectable { viewModel.obtainProfile() - viewModel.profileData.observe(this) { profileRes -> - when (profileRes) { - is Success -> { - val me = profileRes.data - if (me != null) { - binding.displayNameEditText.setText(me.displayName) - binding.noteEditText.setText(me.source?.note) - binding.lockedCheckBox.isChecked = me.locked + lifecycleScope.launch { + viewModel.profileData.collect { profileRes -> + if (profileRes == null) return@collect + when (profileRes) { + is Success -> { + val me = profileRes.data + if (me != null) { + binding.displayNameEditText.setText(me.displayName) + binding.noteEditText.setText(me.source?.note) + binding.lockedCheckBox.isChecked = me.locked - accountFieldEditAdapter.setFields(me.source?.fields.orEmpty()) - binding.addFieldButton.isVisible = - (me.source?.fields?.size ?: 0) < maxAccountFields + accountFieldEditAdapter.setFields(me.source?.fields.orEmpty()) + binding.addFieldButton.isVisible = + (me.source?.fields?.size ?: 0) < maxAccountFields - if (viewModel.avatarData.value == null) { - Glide.with(this) - .load(me.avatar) - .placeholder(R.drawable.avatar_default) - .transform( - FitCenter(), - RoundedCorners( - resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp) + if (viewModel.avatarData.value == null) { + Glide.with(this@EditProfileActivity) + .load(me.avatar) + .placeholder(R.drawable.avatar_default) + .transform( + FitCenter(), + RoundedCorners( + resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp) + ) ) - ) - .into(binding.avatarPreview) - } + .into(binding.avatarPreview) + } - if (viewModel.headerData.value == null) { - Glide.with(this) - .load(me.header) - .into(binding.headerPreview) + if (viewModel.headerData.value == null) { + Glide.with(this@EditProfileActivity) + .load(me.header) + .into(binding.headerPreview) + } } } + is Error -> { + Snackbar.make( + binding.avatarButton, + R.string.error_generic, + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_retry) { + viewModel.obtainProfile() + } + .show() + } + is Loading -> { } } - is Error -> { - Snackbar.make( - binding.avatarButton, - R.string.error_generic, - Snackbar.LENGTH_LONG - ) - .setAction(R.string.action_retry) { - viewModel.obtainProfile() - } - .show() - } - is Loading -> { } } } @@ -215,18 +218,19 @@ class EditProfileActivity : BaseActivity(), Injectable { observeImage(viewModel.avatarData, binding.avatarPreview, true) observeImage(viewModel.headerData, binding.headerPreview, false) - viewModel.saveData.observe( - this - ) { - when (it) { - is Success -> { - finish() - } - is Loading -> { - binding.saveProgressBar.visibility = View.VISIBLE - } - is Error -> { - onSaveFailure(it.errorMessage) + lifecycleScope.launch { + viewModel.saveData.collect { + if (it == null) return@collect + when (it) { + is Success -> { + finish() + } + is Loading -> { + binding.saveProgressBar.visibility = View.VISIBLE + } + is Error -> { + onSaveFailure(it.errorMessage) + } } } } @@ -269,30 +273,30 @@ class EditProfileActivity : BaseActivity(), Injectable { } private fun observeImage( - liveData: LiveData, + flow: StateFlow, imageView: ImageView, roundedCorners: Boolean ) { - liveData.observe( - this - ) { imageUri -> + lifecycleScope.launch { + flow.collect { imageUri -> - // skipping all caches so we can always reuse the same uri - val glide = Glide.with(imageView) - .load(imageUri) - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) + // skipping all caches so we can always reuse the same uri + val glide = Glide.with(imageView) + .load(imageUri) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) - if (roundedCorners) { - glide.transform( - FitCenter(), - RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) - ).into(imageView) - } else { - glide.into(imageView) + if (roundedCorners) { + glide.transform( + FitCenter(), + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) + ).into(imageView) + } else { + glide.into(imageView) + } + + imageView.show() } - - imageView.show() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt index 70f6669ba..35ec65c7a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt @@ -48,6 +48,7 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.updatePadding import androidx.core.widget.doAfterTextChanged +import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.MarginPageTransformer @@ -109,6 +110,7 @@ import java.text.SimpleDateFormat import java.util.Locale import javax.inject.Inject import kotlin.math.abs +import kotlinx.coroutines.launch class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider, HasAndroidInjector, LinkListener { @@ -429,10 +431,32 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide * Subscribe to data loaded at the view model */ private fun subscribeObservables() { - viewModel.accountData.observe(this) { - when (it) { - is Success -> onAccountChanged(it.data) - is Error -> { + lifecycleScope.launch { + viewModel.accountData.collect { + if (it == null) return@collect + when (it) { + is Success -> onAccountChanged(it.data) + is Error -> { + Snackbar.make( + binding.accountCoordinatorLayout, + R.string.error_generic, + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_retry) { viewModel.refresh() } + .show() + } + is Loading -> { } + } + } + } + lifecycleScope.launch { + viewModel.relationshipData.collect { + val relation = it?.data + if (relation != null) { + onRelationshipChanged(relation) + } + + if (it is Error) { Snackbar.make( binding.accountCoordinatorLayout, R.string.error_generic, @@ -441,27 +465,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide .setAction(R.string.action_retry) { viewModel.refresh() } .show() } - is Loading -> { } } } - viewModel.relationshipData.observe(this) { - val relation = it?.data - if (relation != null) { - onRelationshipChanged(relation) + lifecycleScope.launch { + viewModel.noteSaved.collect { + binding.saveNoteInfo.visible(it, View.INVISIBLE) } - - if (it is Error) { - Snackbar.make( - binding.accountCoordinatorLayout, - R.string.error_generic, - Snackbar.LENGTH_LONG - ) - .setAction(R.string.action_retry) { viewModel.refresh() } - .show() - } - } - viewModel.noteSaved.observe(this) { - binding.saveNoteInfo.visible(it, View.INVISIBLE) } // "Post failed" dialog should display in this activity @@ -478,10 +487,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide */ private fun setupRefreshLayout() { binding.swipeToRefreshLayout.setOnRefreshListener { onRefresh() } - viewModel.isRefreshing.observe( - this - ) { isRefreshing -> - binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true + lifecycleScope.launch { + viewModel.isRefreshing.collect { isRefreshing -> + binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true + } } binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt index 0bd622b9e..c21e679d2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt @@ -1,7 +1,6 @@ package com.keylesspalace.tusky.components.account import android.util.Log -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import at.connyduck.calladapter.networkresult.fold @@ -23,6 +22,9 @@ import com.keylesspalace.tusky.util.getDomain import javax.inject.Inject import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch class AccountViewModel @Inject constructor( @@ -31,12 +33,18 @@ class AccountViewModel @Inject constructor( accountManager: AccountManager ) : ViewModel() { - val accountData = MutableLiveData>() - val relationshipData = MutableLiveData>() + private val _accountData = MutableStateFlow(null as Resource?) + val accountData: StateFlow?> = _accountData.asStateFlow() - val noteSaved = MutableLiveData() + private val _relationshipData = MutableStateFlow(null as Resource?) + val relationshipData: StateFlow?> = _relationshipData.asStateFlow() + + private val _noteSaved = MutableStateFlow(false) + val noteSaved: StateFlow = _noteSaved.asStateFlow() + + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow = _isRefreshing.asStateFlow() - val isRefreshing = MutableLiveData().apply { value = false } private var isDataLoading = false lateinit var accountId: String @@ -55,17 +63,17 @@ class AccountViewModel @Inject constructor( init { viewModelScope.launch { eventHub.events.collect { event -> - if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) { - accountData.postValue(Success(event.newProfileData)) + if (event is ProfileEditedEvent && event.newProfileData.id == _accountData.value?.data?.id) { + _accountData.value = Success(event.newProfileData) } } } } private fun obtainAccount(reload: Boolean = false) { - if (accountData.value == null || reload) { + if (_accountData.value == null || reload) { isDataLoading = true - accountData.postValue(Loading()) + _accountData.value = Loading() viewModelScope.launch { mastodonApi.account(accountId) @@ -74,15 +82,15 @@ class AccountViewModel @Inject constructor( domain = getDomain(account.url) isFromOwnDomain = domain == activeAccount.domain - accountData.postValue(Success(account)) + _accountData.value = Success(account) isDataLoading = false - isRefreshing.postValue(false) + _isRefreshing.value = false }, { t -> Log.w(TAG, "failed obtaining account", t) - accountData.postValue(Error(cause = t)) + _accountData.value = Error(cause = t) isDataLoading = false - isRefreshing.postValue(false) + _isRefreshing.value = false } ) } @@ -90,14 +98,14 @@ class AccountViewModel @Inject constructor( } private fun obtainRelationship(reload: Boolean = false) { - if (relationshipData.value == null || reload) { - relationshipData.postValue(Loading()) + if (_relationshipData.value == null || reload) { + _relationshipData.value = Loading() viewModelScope.launch { mastodonApi.relationships(listOf(accountId)) .fold( { relationships -> - relationshipData.postValue( + _relationshipData.value = if (relationships.isNotEmpty()) { Success( relationships[0] @@ -105,11 +113,10 @@ class AccountViewModel @Inject constructor( } else { Error() } - ) }, { t -> Log.w(TAG, "failed obtaining relationships", t) - relationshipData.postValue(Error(cause = t)) + _relationshipData.value = Error(cause = t) } ) } @@ -117,7 +124,7 @@ class AccountViewModel @Inject constructor( } fun changeFollowState() { - val relationship = relationshipData.value?.data + val relationship = _relationshipData.value?.data if (relationship?.following == true || relationship?.requested == true) { changeRelationship(RelationShipAction.UNFOLLOW) } else { @@ -126,7 +133,7 @@ class AccountViewModel @Inject constructor( } fun changeBlockState() { - if (relationshipData.value?.data?.blocking == true) { + if (_relationshipData.value?.data?.blocking == true) { changeRelationship(RelationShipAction.UNBLOCK) } else { changeRelationship(RelationShipAction.BLOCK) @@ -142,7 +149,7 @@ class AccountViewModel @Inject constructor( } fun changeSubscribingState() { - val relationship = relationshipData.value?.data + val relationship = _relationshipData.value?.data if (relationship?.notifying == true || // Mastodon 3.3.0rc1 relationship?.subscribing == true // Pleroma ) { @@ -156,9 +163,9 @@ class AccountViewModel @Inject constructor( viewModelScope.launch { mastodonApi.blockDomain(instance).fold({ eventHub.dispatch(DomainMuteEvent(instance)) - val relation = relationshipData.value?.data + val relation = _relationshipData.value?.data if (relation != null) { - relationshipData.postValue(Success(relation.copy(blockingDomain = true))) + _relationshipData.value = Success(relation.copy(blockingDomain = true)) } }, { e -> Log.e(TAG, "Error muting $instance", e) @@ -169,9 +176,9 @@ class AccountViewModel @Inject constructor( fun unblockDomain(instance: String) { viewModelScope.launch { mastodonApi.unblockDomain(instance).fold({ - val relation = relationshipData.value?.data + val relation = _relationshipData.value?.data if (relation != null) { - relationshipData.postValue(Success(relation.copy(blockingDomain = false))) + _relationshipData.value = Success(relation.copy(blockingDomain = false)) } }, { e -> Log.e(TAG, "Error unmuting $instance", e) @@ -180,7 +187,7 @@ class AccountViewModel @Inject constructor( } fun changeShowReblogsState() { - if (relationshipData.value?.data?.showingReblogs == true) { + if (_relationshipData.value?.data?.showingReblogs == true) { changeRelationship(RelationShipAction.FOLLOW, false) } else { changeRelationship(RelationShipAction.FOLLOW, true) @@ -195,9 +202,9 @@ class AccountViewModel @Inject constructor( parameter: Boolean? = null, duration: Int? = null ) = viewModelScope.launch { - val relation = relationshipData.value?.data - val account = accountData.value?.data - val isMastodon = relationshipData.value?.data?.notifying != null + val relation = _relationshipData.value?.data + val account = _accountData.value?.data + val isMastodon = _relationshipData.value?.data?.notifying != null if (relation != null && account != null) { // optimistically post new state for faster response @@ -230,7 +237,7 @@ class AccountViewModel @Inject constructor( } } } - relationshipData.postValue(Loading(newRelation)) + _relationshipData.value = Loading(newRelation) } val relationshipCall = when (relationshipAction) { @@ -265,7 +272,7 @@ class AccountViewModel @Inject constructor( relationshipCall.fold( { relationship -> - relationshipData.postValue(Success(relationship)) + _relationshipData.value = Success(relationship) when (relationshipAction) { RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId)) @@ -276,22 +283,22 @@ class AccountViewModel @Inject constructor( }, { t -> Log.w(TAG, "failed loading relationship", t) - relationshipData.postValue(Error(relation, cause = t)) + _relationshipData.value = Error(relation, cause = t) } ) } fun noteChanged(newNote: String) { - noteSaved.postValue(false) + _noteSaved.value = false noteUpdateJob?.cancel() noteUpdateJob = viewModelScope.launch { delay(1500) mastodonApi.updateAccountNote(accountId, newNote) .fold( { - noteSaved.postValue(true) + _noteSaved.value = true delay(4000) - noteSaved.postValue(false) + _noteSaved.value = false }, { t -> Log.w(TAG, "Error updating note", t) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt index 2c41a1ef4..1ed3b3779 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt @@ -26,6 +26,7 @@ import android.view.View import android.widget.PopupWindow import androidx.activity.viewModels import androidx.core.view.MenuProvider +import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -53,6 +54,7 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import javax.inject.Inject +import kotlinx.coroutines.launch class AnnouncementsActivity : BottomSheetActivity(), @@ -111,41 +113,46 @@ class AnnouncementsActivity : binding.announcementsList.adapter = adapter - viewModel.announcements.observe(this) { - when (it) { - is Success -> { - binding.progressBar.hide() - binding.swipeRefreshLayout.isRefreshing = false - if (it.data.isNullOrEmpty()) { - binding.errorMessageView.setup( - R.drawable.elephant_friend_empty, - R.string.no_announcements - ) - binding.errorMessageView.show() - } else { + lifecycleScope.launch { + viewModel.announcements.collect { + if (it == null) return@collect + when (it) { + is Success -> { + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + if (it.data.isNullOrEmpty()) { + binding.errorMessageView.setup( + R.drawable.elephant_friend_empty, + R.string.no_announcements + ) + binding.errorMessageView.show() + } else { + binding.errorMessageView.hide() + } + adapter.updateList(it.data ?: listOf()) + } + is Loading -> { binding.errorMessageView.hide() } - adapter.updateList(it.data ?: listOf()) - } - is Loading -> { - binding.errorMessageView.hide() - } - is Error -> { - binding.progressBar.hide() - binding.swipeRefreshLayout.isRefreshing = false - binding.errorMessageView.setup( - R.drawable.errorphant_error, - R.string.error_generic - ) { - refreshAnnouncements() + is Error -> { + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + binding.errorMessageView.setup( + R.drawable.errorphant_error, + R.string.error_generic + ) { + refreshAnnouncements() + } + binding.errorMessageView.show() } - binding.errorMessageView.show() } } } - viewModel.emojis.observe(this) { - picker.adapter = EmojiAdapter(it, this, animateEmojis) + lifecycleScope.launch { + viewModel.emoji.collect { + picker.adapter = EmojiAdapter(it, this@AnnouncementsActivity, animateEmojis) + } } viewModel.load() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt index 9fad312eb..1e1503c6e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt @@ -16,8 +16,6 @@ package com.keylesspalace.tusky.components.announcements import android.util.Log -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import at.connyduck.calladapter.networkresult.fold @@ -32,6 +30,9 @@ import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.Success import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch class AnnouncementsViewModel @Inject constructor( @@ -40,25 +41,25 @@ class AnnouncementsViewModel @Inject constructor( private val eventHub: EventHub ) : ViewModel() { - private val announcementsMutable = MutableLiveData>>() - val announcements: LiveData>> = announcementsMutable + private val _announcements = MutableStateFlow(null as Resource>?) + val announcements: StateFlow>?> = _announcements.asStateFlow() - private val emojisMutable = MutableLiveData>() - val emojis: LiveData> = emojisMutable + private val _emoji = MutableStateFlow(emptyList()) + val emoji: StateFlow> = _emoji.asStateFlow() init { viewModelScope.launch { - emojisMutable.postValue(instanceInfoRepo.getEmojis()) + _emoji.value = instanceInfoRepo.getEmojis() } } fun load() { viewModelScope.launch { - announcementsMutable.postValue(Loading()) + _announcements.value = Loading() mastodonApi.listAnnouncements() .fold( { - announcementsMutable.postValue(Success(it)) + _announcements.value = Success(it) it.filter { announcement -> !announcement.read } .forEach { announcement -> mastodonApi.dismissAnnouncement(announcement.id) @@ -79,7 +80,7 @@ class AnnouncementsViewModel @Inject constructor( } }, { - announcementsMutable.postValue(Error(cause = it)) + _announcements.value = Error(cause = it) } ) } @@ -90,9 +91,9 @@ class AnnouncementsViewModel @Inject constructor( mastodonApi.addAnnouncementReaction(announcementId, name) .fold( { - announcementsMutable.postValue( + _announcements.value = Success( - announcements.value!!.data!!.map { announcement -> + announcements.value?.data?.map { announcement -> if (announcement.id == announcementId) { announcement.copy( reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) { @@ -109,7 +110,7 @@ class AnnouncementsViewModel @Inject constructor( } else { listOf( *announcement.reactions.toTypedArray(), - emojis.value!!.find { emoji -> emoji.shortcode == name }!!.run { + emoji.value.find { emoji -> emoji.shortcode == name }!!.run { Announcement.Reaction( name, 1, @@ -126,7 +127,6 @@ class AnnouncementsViewModel @Inject constructor( } } ) - ) }, { Log.w(TAG, "Failed to add reaction to the announcement.", it) @@ -140,7 +140,7 @@ class AnnouncementsViewModel @Inject constructor( mastodonApi.removeAnnouncementReaction(announcementId, name) .fold( { - announcementsMutable.postValue( + _announcements.value = Success( announcements.value!!.data!!.map { announcement -> if (announcement.id == announcementId) { @@ -165,7 +165,6 @@ class AnnouncementsViewModel @Inject constructor( } } ) - ) }, { Log.w(TAG, "Failed to remove reaction from the announcement.", it) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt index 1baf15784..72ebbd203 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt @@ -19,6 +19,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter @@ -28,6 +29,7 @@ import com.keylesspalace.tusky.util.viewBinding import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import javax.inject.Inject +import kotlinx.coroutines.launch class ReportActivity : BottomSheetActivity(), HasAndroidInjector { @@ -82,8 +84,9 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { } private fun subscribeObservables() { - viewModel.navigation.observe(this) { screen -> - if (screen != null) { + lifecycleScope.launch { + viewModel.navigation.collect { screen -> + if (screen == null) return@collect viewModel.navigated() when (screen) { Screen.Statuses -> showStatusesPage() @@ -95,10 +98,12 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { } } - viewModel.checkUrl.observe(this) { - if (!it.isNullOrBlank()) { - viewModel.urlChecked() - viewUrl(it) + lifecycleScope.launch { + viewModel.checkUrl.collect { + if (!it.isNullOrBlank()) { + viewModel.urlChecked() + viewUrl(it) + } } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt index 96b169c3f..b2b84d5ae 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -15,8 +15,6 @@ package com.keylesspalace.tusky.components.report -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.Pager @@ -40,6 +38,9 @@ import com.keylesspalace.tusky.util.toViewData import javax.inject.Inject import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -49,20 +50,20 @@ class ReportViewModel @Inject constructor( private val eventHub: EventHub ) : ViewModel() { - private val navigationMutable = MutableLiveData() - val navigation: LiveData = navigationMutable + private val navigationMutable = MutableStateFlow(null as Screen?) + val navigation: StateFlow = navigationMutable.asStateFlow() - private val muteStateMutable = MutableLiveData>() - val muteState: LiveData> = muteStateMutable + private val muteStateMutable = MutableStateFlow(null as Resource?) + val muteState: StateFlow?> = muteStateMutable.asStateFlow() - private val blockStateMutable = MutableLiveData>() - val blockState: LiveData> = blockStateMutable + private val blockStateMutable = MutableStateFlow(null as Resource?) + val blockState: StateFlow?> = blockStateMutable.asStateFlow() - private val reportingStateMutable = MutableLiveData>() - var reportingState: LiveData> = reportingStateMutable + private val reportingStateMutable = MutableStateFlow(null as Resource?) + var reportingState: StateFlow?> = reportingStateMutable.asStateFlow() - private val checkUrlMutable = MutableLiveData() - val checkUrl: LiveData = checkUrlMutable + private val checkUrlMutable = MutableStateFlow(null as String?) + val checkUrl: StateFlow = checkUrlMutable.asStateFlow() private val accountIdFlow = MutableSharedFlow( replay = 1, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt index 0f8065776..2b5ed2628 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt @@ -19,6 +19,7 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.Screen @@ -30,6 +31,7 @@ import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import javax.inject.Inject +import kotlinx.coroutines.launch class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { @@ -47,37 +49,43 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { } private fun subscribeObservables() { - viewModel.muteState.observe(viewLifecycleOwner) { - if (it !is Loading) { - binding.buttonMute.show() - binding.progressMute.show() - } else { - binding.buttonMute.hide() - binding.progressMute.hide() - } - - binding.buttonMute.setText( - when (it.data) { - true -> R.string.action_unmute - else -> R.string.action_mute + viewLifecycleOwner.lifecycleScope.launch { + viewModel.muteState.collect { + if (it == null) return@collect + if (it !is Loading) { + binding.buttonMute.show() + binding.progressMute.show() + } else { + binding.buttonMute.hide() + binding.progressMute.hide() } - ) + + binding.buttonMute.setText( + when (it.data) { + true -> R.string.action_unmute + else -> R.string.action_mute + } + ) + } } - viewModel.blockState.observe(viewLifecycleOwner) { - if (it !is Loading) { - binding.buttonBlock.show() - binding.progressBlock.show() - } else { - binding.buttonBlock.hide() - binding.progressBlock.hide() - } - binding.buttonBlock.setText( - when (it.data) { - true -> R.string.action_unblock - else -> R.string.action_block + viewLifecycleOwner.lifecycleScope.launch { + viewModel.blockState.collect { + if (it == null) return@collect + if (it !is Loading) { + binding.buttonBlock.show() + binding.progressBlock.show() + } else { + binding.buttonBlock.hide() + binding.progressBlock.hide() } - ) + binding.buttonBlock.setText( + when (it.data) { + true -> R.string.action_unblock + else -> R.string.action_block + } + ) + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt index 1b303d7dd..215414ff9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt @@ -20,6 +20,7 @@ import android.view.View import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel @@ -35,6 +36,7 @@ import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import java.io.IOException import javax.inject.Inject +import kotlinx.coroutines.launch class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { @@ -79,11 +81,14 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { } private fun subscribeObservables() { - viewModel.reportingState.observe(viewLifecycleOwner) { - when (it) { - is Success -> viewModel.navigateTo(Screen.Done) - is Loading -> showLoading() - is Error -> showError(it.cause) + viewLifecycleOwner.lifecycleScope.launch { + viewModel.reportingState.collect { + if (it == null) return@collect + when (it) { + is Success -> viewModel.navigateTo(Screen.Done) + is Loading -> showLoading() + is Error -> showError(it.cause) + } } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt index 4df024469..4d7239d02 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt @@ -15,12 +15,12 @@ package com.keylesspalace.tusky.db -import androidx.lifecycle.LiveData import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import kotlinx.coroutines.flow.Flow @Dao interface DraftDao { @@ -32,7 +32,7 @@ interface DraftDao { fun draftsPagingSource(accountId: Long): PagingSource @Query("SELECT COUNT(*) FROM DraftEntity WHERE accountId = :accountId AND failedToSendNew = 1") - fun draftsNeedUserAlert(accountId: Long): LiveData + fun draftsNeedUserAlert(accountId: Long): Flow @Query( "UPDATE DraftEntity SET failedToSendNew = 0 WHERE accountId = :accountId AND failedToSendNew = 1" diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt index 9b9bcb7cd..5b2993f68 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt @@ -51,36 +51,37 @@ class DraftsAlert @Inject constructor(db: AppDatabase) { // Assume a single MainActivity, AccountActivity or DraftsActivity never sees more then one user id in its lifetime. val activeAccountId = activeAccount.id - // This LiveData will be automatically disposed when the activity is destroyed. val draftsNeedUserAlert = draftDao.draftsNeedUserAlert(activeAccountId) // observe ensures that this gets called at the most appropriate moment wrt the context lifecycle— // at init, at next onResume, or immediately if the context is resumed already. - if (showAlert) { - draftsNeedUserAlert.observe(context) { count -> - Log.d(TAG, "User id $activeAccountId changed: Notification-worthy draft count $count") - if (count > 0) { - AlertDialog.Builder(context) - .setTitle(R.string.action_post_failed) - .setMessage( - context.resources.getQuantityString(R.plurals.action_post_failed_detail, count) - ) - .setPositiveButton(R.string.action_post_failed_show_drafts) { _: DialogInterface?, _: Int -> - clearDraftsAlert(coroutineScope, activeAccountId) // User looked at drafts + coroutineScope.launch { + if (showAlert) { + draftsNeedUserAlert.collect { count -> + Log.d(TAG, "User id $activeAccountId changed: Notification-worthy draft count $count") + if (count > 0) { + AlertDialog.Builder(context) + .setTitle(R.string.action_post_failed) + .setMessage( + context.resources.getQuantityString(R.plurals.action_post_failed_detail, count) + ) + .setPositiveButton(R.string.action_post_failed_show_drafts) { _: DialogInterface?, _: Int -> + clearDraftsAlert(coroutineScope, activeAccountId) // User looked at drafts - val intent = DraftsActivity.newIntent(context) - context.startActivity(intent) - } - .setNegativeButton(R.string.action_post_failed_do_nothing) { _: DialogInterface?, _: Int -> - clearDraftsAlert(coroutineScope, activeAccountId) // User doesn't care - } - .show() + val intent = DraftsActivity.newIntent(context) + context.startActivity(intent) + } + .setNegativeButton(R.string.action_post_failed_do_nothing) { _: DialogInterface?, _: Int -> + clearDraftsAlert(coroutineScope, activeAccountId) // User doesn't care + } + .show() + } + } + } else { + draftsNeedUserAlert.collect { + Log.d(TAG, "User id $activeAccountId: Clean out notification-worthy drafts") + clearDraftsAlert(coroutineScope, activeAccountId) } - } - } else { - draftsNeedUserAlert.observe(context) { - Log.d(TAG, "User id $activeAccountId: Clean out notification-worthy drafts") - clearDraftsAlert(coroutineScope, activeAccountId) } } } ?: run { diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt index d3ac80baa..b04f5fdf6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -18,7 +18,6 @@ package com.keylesspalace.tusky.viewmodel import android.app.Application import android.net.Uri import androidx.core.net.toUri -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import at.connyduck.calladapter.networkresult.fold @@ -40,6 +39,7 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.shareIn @@ -66,10 +66,17 @@ class EditProfileViewModel @Inject constructor( private val instanceInfoRepo: InstanceInfoRepository ) : ViewModel() { - val profileData = MutableLiveData>() - val avatarData = MutableLiveData() - val headerData = MutableLiveData() - val saveData = MutableLiveData>() + private val _profileData = MutableStateFlow(null as Resource?) + val profileData: StateFlow?> = _profileData.asStateFlow() + + private val _avatarData = MutableStateFlow(null as Uri?) + val avatarData: StateFlow = _avatarData.asStateFlow() + + private val _headerData = MutableStateFlow(null as Uri?) + val headerData: StateFlow = _headerData.asStateFlow() + + private val _saveData = MutableStateFlow(null as Resource?) + val saveData: StateFlow?> = _saveData.asStateFlow() val instanceData: Flow = instanceInfoRepo::getUpdatedInstanceInfoOrFallback.asFlow() .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) @@ -80,16 +87,16 @@ class EditProfileViewModel @Inject constructor( private var apiProfileAccount: Account? = null fun obtainProfile() = viewModelScope.launch { - if (profileData.value == null || profileData.value is Error) { - profileData.postValue(Loading()) + if (_profileData.value == null || _profileData.value is Error) { + _profileData.value = Loading() mastodonApi.accountVerifyCredentials().fold( { profile -> apiProfileAccount = profile - profileData.postValue(Success(profile)) + _profileData.value = Success(profile) }, { - profileData.postValue(Error()) + _profileData.value = Error() } ) } @@ -100,11 +107,11 @@ class EditProfileViewModel @Inject constructor( fun getHeaderUri() = getCacheFileForName(HEADER_FILE_NAME).toUri() fun newAvatarPicked() { - avatarData.value = getAvatarUri() + _avatarData.value = getAvatarUri() } fun newHeaderPicked() { - headerData.value = getHeaderUri() + _headerData.value = getHeaderUri() } internal fun dataChanged(newProfileData: ProfileDataInUi) { @@ -112,16 +119,16 @@ class EditProfileViewModel @Inject constructor( } internal fun save(newProfileData: ProfileDataInUi) { - if (saveData.value is Loading || profileData.value !is Success) { + if (_saveData.value is Loading || _profileData.value !is Success) { return } - saveData.value = Loading() + _saveData.value = Loading() val diff = getProfileDiff(apiProfileAccount, newProfileData) if (!diff.hasChanges()) { // if nothing has changed, there is no need to make an api call - saveData.value = Success() + _saveData.value = Success() return } @@ -160,11 +167,11 @@ class EditProfileViewModel @Inject constructor( diff.field4?.second?.toRequestBody(MultipartBody.FORM) ).fold( { newAccountData -> - saveData.postValue(Success()) + _saveData.value = Success() eventHub.dispatch(ProfileEditedEvent(newAccountData)) }, { throwable -> - saveData.postValue(Error(errorMessage = throwable.getServerErrorMessage())) + _saveData.value = Error(errorMessage = throwable.getServerErrorMessage()) } ) } @@ -172,18 +179,18 @@ class EditProfileViewModel @Inject constructor( // cache activity state for rotation change internal fun updateProfile(newProfileData: ProfileDataInUi) { - if (profileData.value is Success) { - val newProfileSource = profileData.value?.data?.source?.copy( + if (_profileData.value is Success) { + val newProfileSource = _profileData.value?.data?.source?.copy( note = newProfileData.note, fields = newProfileData.fields ) - val newProfile = profileData.value?.data?.copy( + val newProfile = _profileData.value?.data?.copy( displayName = newProfileData.displayName, locked = newProfileData.locked, source = newProfileSource ) - profileData.value = Success(newProfile) + _profileData.value = Success(newProfile) } } @@ -209,13 +216,13 @@ class EditProfileViewModel @Inject constructor( newProfileData.locked } - val avatarFile = if (avatarData.value != null) { + val avatarFile = if (_avatarData.value != null) { getCacheFileForName(AVATAR_FILE_NAME) } else { null } - val headerFile = if (headerData.value != null) { + val headerFile = if (_headerData.value != null) { getCacheFileForName(HEADER_FILE_NAME) } else { null diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b3b0a4447..1174a7f73 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -75,7 +75,6 @@ androidx-emoji2-view-helper = { module = "androidx.emoji2:emoji2-views-helper", androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "androidx-exifinterface" } androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" } androidx-lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidx-lifecycle" } -androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-reactivestreams-ktx = { module = "androidx.lifecycle:lifecycle-reactivestreams-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" } @@ -137,7 +136,7 @@ xmlwriter = { module = "org.pageseeder.xmlwriter:pso-xmlwriter", version.ref = " androidx = ["androidx-core-ktx", "androidx-appcompat", "androidx-fragment-ktx", "androidx-browser", "androidx-swiperefreshlayout", "androidx-recyclerview", "androidx-exifinterface", "androidx-cardview", "androidx-preference-ktx", "androidx-sharetarget", "androidx-emoji2-core", "androidx-emoji2-views-core", "androidx-emoji2-view-helper", "androidx-lifecycle-viewmodel-ktx", - "androidx-lifecycle-livedata-ktx", "androidx-lifecycle-common-java8", "androidx-lifecycle-reactivestreams-ktx", + "androidx-lifecycle-common-java8", "androidx-lifecycle-reactivestreams-ktx", "androidx-constraintlayout", "androidx-paging-runtime-ktx", "androidx-viewpager2", "androidx-work-runtime-ktx", "androidx-core-splashscreen", "androidx-activity", "androidx-media3-exoplayer", "androidx-media3-datasource-okhttp", "androidx-media3-ui"]