diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt index 31dedc59c..94a0c59c2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt @@ -3,6 +3,7 @@ package com.keylesspalace.tusky.components.filters import android.content.DialogInterface.BUTTON_POSITIVE import android.content.Intent import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.lifecycle.lifecycleScope import com.keylesspalace.tusky.BaseActivity @@ -10,10 +11,11 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ActivityFiltersBinding import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.launchAndRepeatOnLifecycle import com.keylesspalace.tusky.util.show -import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.util.withSlideInAnimation import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -23,6 +25,12 @@ class FiltersActivity : BaseActivity(), FiltersListener { private val binding by viewBinding(ActivityFiltersBinding::inflate) private val viewModel: FiltersViewModel by viewModels() + private val editFilterLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + // refresh the filters upon returning from EditFilterActivity + reloadFilters() + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -38,20 +46,16 @@ class FiltersActivity : BaseActivity(), FiltersListener { launchEditFilterActivity() } - binding.swipeRefreshLayout.setOnRefreshListener { loadFilters() } + binding.swipeRefreshLayout.setOnRefreshListener { reloadFilters() } binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) setTitle(R.string.pref_title_timeline_filters) - } - override fun onResume() { - super.onResume() - loadFilters() observeViewModel() } private fun observeViewModel() { - lifecycleScope.launch { + launchAndRepeatOnLifecycle { viewModel.state.collect { state -> binding.progressBar.visible( state.loadingState == FiltersViewModel.LoadingState.LOADING @@ -68,7 +72,7 @@ class FiltersActivity : BaseActivity(), FiltersListener { R.drawable.errorphant_offline, R.string.error_network ) { - loadFilters() + reloadFilters() } binding.messageView.show() } @@ -77,7 +81,7 @@ class FiltersActivity : BaseActivity(), FiltersListener { R.drawable.errorphant_error, R.string.error_generic ) { - loadFilters() + reloadFilters() } binding.messageView.show() } @@ -99,8 +103,8 @@ class FiltersActivity : BaseActivity(), FiltersListener { } } - private fun loadFilters() { - viewModel.load() + private fun reloadFilters() { + viewModel.reload() } private fun launchEditFilterActivity(filter: Filter? = null) { @@ -108,8 +112,8 @@ class FiltersActivity : BaseActivity(), FiltersListener { if (filter != null) { putExtra(EditFilterActivity.FILTER_TO_EDIT, filter) } - } - startActivityWithSlideInAnimation(intent) + }.withSlideInAnimation() + editFilterLauncher.launch(intent) } override fun deleteFilter(filter: Filter) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersViewModel.kt index 39c802039..5c28031e5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersViewModel.kt @@ -15,6 +15,9 @@ import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @HiltViewModel @@ -36,10 +39,18 @@ class FiltersViewModel @Inject constructor( private val _state = MutableStateFlow(State(emptyList(), LoadingState.INITIAL)) val state: StateFlow = _state.asStateFlow() - fun load() { - this@FiltersViewModel._state.value = _state.value.copy(loadingState = LoadingState.LOADING) + private val loadTrigger = MutableStateFlow(0) + init { viewModelScope.launch { + observeLoad() + } + } + + private suspend fun observeLoad() { + loadTrigger.collectLatest { + this@FiltersViewModel._state.update { it.copy(loadingState = LoadingState.LOADING) } + api.getFilters().fold( { filters -> this@FiltersViewModel._state.value = State(filters, LoadingState.LOADED) @@ -52,60 +63,65 @@ class FiltersViewModel @Inject constructor( }, { _ -> // TODO log errors (also below) - this@FiltersViewModel._state.value = _state.value.copy(loadingState = LoadingState.ERROR_OTHER) + this@FiltersViewModel._state.update { it.copy(loadingState = LoadingState.ERROR_OTHER) } } ) - this@FiltersViewModel._state.value = _state.value.copy(loadingState = LoadingState.ERROR_OTHER) + this@FiltersViewModel._state.update { it.copy(loadingState = LoadingState.ERROR_OTHER) } } else { - this@FiltersViewModel._state.value = _state.value.copy(loadingState = LoadingState.ERROR_NETWORK) + this@FiltersViewModel._state.update { it.copy(loadingState = LoadingState.ERROR_NETWORK) } } } ) } } - fun deleteFilter(filter: Filter, parent: View) { - viewModelScope.launch { - api.deleteFilter(filter.id).fold( - { - this@FiltersViewModel._state.value = State( - this@FiltersViewModel._state.value.filters.filter { - it.id != filter.id - }, + fun reload() { + loadTrigger.update { it + 1 } + } + + suspend fun deleteFilter(filter: Filter, parent: View) { + // First wait for a pending loading operation to complete + _state.first { it.loadingState > LoadingState.LOADING } + + api.deleteFilter(filter.id).fold( + { + this@FiltersViewModel._state.update { currentState -> + State( + currentState.filters.filter { it.id != filter.id }, LoadingState.LOADED ) - for (context in filter.context) { - eventHub.dispatch(PreferenceChangedEvent(context)) - } - }, - { throwable -> - if (throwable.isHttpNotFound()) { - api.deleteFilterV1(filter.id).fold( - { - this@FiltersViewModel._state.value = State( - this@FiltersViewModel._state.value.filters.filter { - it.id != filter.id - }, + } + for (context in filter.context) { + eventHub.dispatch(PreferenceChangedEvent(context)) + } + }, + { throwable -> + if (throwable.isHttpNotFound()) { + api.deleteFilterV1(filter.id).fold( + { + this@FiltersViewModel._state.update { currentState -> + State( + currentState.filters.filter { it.id != filter.id }, LoadingState.LOADED ) - }, - { - Snackbar.make( - parent, - "Error deleting filter '${filter.title}'", - Snackbar.LENGTH_SHORT - ).show() } - ) - } else { - Snackbar.make( - parent, - "Error deleting filter '${filter.title}'", - Snackbar.LENGTH_SHORT - ).show() - } + }, + { + Snackbar.make( + parent, + "Error deleting filter '${filter.title}'", + Snackbar.LENGTH_SHORT + ).show() + } + ) + } else { + Snackbar.make( + parent, + "Error deleting filter '${filter.title}'", + Snackbar.LENGTH_SHORT + ).show() } - ) - } + } + ) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ActivityExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ActivityExtensions.kt index c59f9ab56..724ea69a4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ActivityExtensions.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ActivityExtensions.kt @@ -12,10 +12,13 @@ import androidx.lifecycle.LifecycleEventObserver import com.keylesspalace.tusky.BaseActivity fun Activity.startActivityWithSlideInAnimation(intent: Intent) { + startActivity(intent.withSlideInAnimation()) +} + +fun Intent.withSlideInAnimation(): Intent { // the new transition api needs to be called by the activity that is the result of the transition, // so we pass a flag that BaseActivity will respect. - intent.putExtra(BaseActivity.OPEN_WITH_SLIDE_IN, true) - startActivity(intent) + return putExtra(BaseActivity.OPEN_WITH_SLIDE_IN, true) } /** diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LifecycleExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/LifecycleExtensions.kt new file mode 100644 index 000000000..c0d99b6ec --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/LifecycleExtensions.kt @@ -0,0 +1,21 @@ +package com.keylesspalace.tusky.util + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.coroutineScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +fun Lifecycle.launchAndRepeatOnLifecycle( + state: Lifecycle.State = Lifecycle.State.STARTED, + block: suspend CoroutineScope.() -> Unit +): Job = coroutineScope.launch { + repeatOnLifecycle(state, block) +} + +fun LifecycleOwner.launchAndRepeatOnLifecycle( + state: Lifecycle.State = Lifecycle.State.STARTED, + block: suspend CoroutineScope.() -> Unit +): Job = lifecycle.launchAndRepeatOnLifecycle(state, block)