forked from melod1n/fast-messenger
refactor(settings): emit navigation and haptic actions as one-off screen effects
This commit is contained in:
@@ -8,6 +8,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
@@ -15,6 +16,7 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
@@ -46,6 +48,12 @@ context(viewModel: ViewModel)
|
|||||||
fun <T> Flow<T>.listenValue(action: suspend (T) -> Unit): Job =
|
fun <T> Flow<T>.listenValue(action: suspend (T) -> Unit): Job =
|
||||||
listenValue(viewModel.viewModelScope, action)
|
listenValue(viewModel.viewModelScope, action)
|
||||||
|
|
||||||
|
context(viewModel: ViewModel)
|
||||||
|
fun <T> MutableSharedFlow<T>.emitOnMain(value: T) {
|
||||||
|
val flow = this
|
||||||
|
viewModel.viewModelScope.launch { flow.emit(value) }
|
||||||
|
}
|
||||||
|
|
||||||
fun <T> Flow<T>.listenValue(
|
fun <T> Flow<T>.listenValue(
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
action: suspend (T) -> Unit
|
action: suspend (T) -> Unit
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import dev.meloda.fast.logger.FastLogger
|
|||||||
import dev.meloda.fast.model.database.AccountEntity
|
import dev.meloda.fast.model.database.AccountEntity
|
||||||
import dev.meloda.fast.settings.model.HapticType
|
import dev.meloda.fast.settings.model.HapticType
|
||||||
import dev.meloda.fast.settings.model.SettingsDialog
|
import dev.meloda.fast.settings.model.SettingsDialog
|
||||||
|
import dev.meloda.fast.settings.model.SettingsEffect
|
||||||
import dev.meloda.fast.settings.model.SettingsIntent
|
import dev.meloda.fast.settings.model.SettingsIntent
|
||||||
import dev.meloda.fast.settings.model.SettingsItem
|
import dev.meloda.fast.settings.model.SettingsItem
|
||||||
import dev.meloda.fast.settings.model.SettingsNavigationIntent
|
import dev.meloda.fast.settings.model.SettingsNavigationIntent
|
||||||
@@ -36,11 +37,10 @@ import dev.meloda.fast.settings.model.SettingsScreenState
|
|||||||
import dev.meloda.fast.settings.model.TextProvider
|
import dev.meloda.fast.settings.model.TextProvider
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class SettingsViewModel(
|
class SettingsViewModel(
|
||||||
@@ -56,11 +56,8 @@ class SettingsViewModel(
|
|||||||
private val screenState = MutableStateFlow(SettingsScreenState.EMPTY)
|
private val screenState = MutableStateFlow(SettingsScreenState.EMPTY)
|
||||||
val screenStateFlow get() = screenState.asStateFlow()
|
val screenStateFlow get() = screenState.asStateFlow()
|
||||||
|
|
||||||
private val hapticType = MutableStateFlow<HapticType?>(null)
|
private val screenEffect = MutableSharedFlow<SettingsEffect>(extraBufferCapacity = 1)
|
||||||
val hapticTypeFlow get() = hapticType.asStateFlow()
|
val screenEffectFlow = screenEffect.asSharedFlow()
|
||||||
|
|
||||||
private val navigationIntent = MutableStateFlow<SettingsNavigationIntent?>(null)
|
|
||||||
val navigationIntentFlow get() = navigationIntent.asStateFlow()
|
|
||||||
|
|
||||||
private val settings = mutableListOf<SettingsItem<*>>()
|
private val settings = mutableListOf<SettingsItem<*>>()
|
||||||
private var showDebugCategory: Boolean = userSettings.showDebugCategory.value
|
private var showDebugCategory: Boolean = userSettings.showDebugCategory.value
|
||||||
@@ -72,11 +69,7 @@ class SettingsViewModel(
|
|||||||
fun handleIntent(intent: SettingsIntent) {
|
fun handleIntent(intent: SettingsIntent) {
|
||||||
when (intent) {
|
when (intent) {
|
||||||
SettingsIntent.BackClick -> {
|
SettingsIntent.BackClick -> {
|
||||||
navigationIntent.setValue { SettingsNavigationIntent.Back }
|
screenEffect.tryEmit(SettingsEffect.Navigate(SettingsNavigationIntent.Back))
|
||||||
}
|
|
||||||
|
|
||||||
SettingsIntent.ConsumePerformHaptic -> {
|
|
||||||
hapticType.setValue { null }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is SettingsIntent.ItemClick -> {
|
is SettingsIntent.ItemClick -> {
|
||||||
@@ -148,7 +141,10 @@ class SettingsViewModel(
|
|||||||
UserConfig.accessToken = oldToken
|
UserConfig.accessToken = oldToken
|
||||||
},
|
},
|
||||||
success = { user ->
|
success = { user ->
|
||||||
if (user == null) return@listenValue
|
if (user == null) {
|
||||||
|
UserConfig.accessToken = oldToken
|
||||||
|
return@listenValue
|
||||||
|
}
|
||||||
|
|
||||||
UserConfig.currentUserId = user.id
|
UserConfig.currentUserId = user.id
|
||||||
|
|
||||||
@@ -169,7 +165,9 @@ class SettingsViewModel(
|
|||||||
|
|
||||||
accountsRepository.storeAccounts(listOf(account))
|
accountsRepository.storeAccounts(listOf(account))
|
||||||
|
|
||||||
navigationIntent.setValue { SettingsNavigationIntent.Restart }
|
screenEffect.tryEmit(
|
||||||
|
SettingsEffect.Navigate(SettingsNavigationIntent.Restart)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -204,26 +202,21 @@ class SettingsViewModel(
|
|||||||
|
|
||||||
private fun onLogOutAlertPositiveClick() {
|
private fun onLogOutAlertPositiveClick() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val tasks = listOf(
|
accountsRepository.storeAccounts(
|
||||||
async {
|
listOf(
|
||||||
accountsRepository.storeAccounts(
|
AccountEntity(
|
||||||
listOf(
|
userId = UserConfig.userId,
|
||||||
AccountEntity(
|
accessToken = "",
|
||||||
userId = UserConfig.userId,
|
fastToken = UserConfig.fastToken,
|
||||||
accessToken = "",
|
trustedHash = UserConfig.trustedHash,
|
||||||
fastToken = UserConfig.fastToken,
|
exchangeToken = null
|
||||||
trustedHash = UserConfig.trustedHash,
|
|
||||||
exchangeToken = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
)
|
||||||
async { UserConfig.clear() }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
tasks.awaitAll()
|
UserConfig.clear()
|
||||||
|
|
||||||
navigationIntent.setValue { SettingsNavigationIntent.LogOut }
|
screenEffect.tryEmit(SettingsEffect.Navigate(SettingsNavigationIntent.LogOut))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +256,7 @@ class SettingsViewModel(
|
|||||||
|
|
||||||
createSettings()
|
createSettings()
|
||||||
|
|
||||||
hapticType.update { HapticType.REJECT }
|
screenEffect.tryEmit(SettingsEffect.PerformHaptic(HapticType.REJECT))
|
||||||
showDebugCategory = false
|
showDebugCategory = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,7 +272,7 @@ class SettingsViewModel(
|
|||||||
|
|
||||||
createSettings()
|
createSettings()
|
||||||
|
|
||||||
hapticType.update { HapticType.LONG_PRESS }
|
screenEffect.tryEmit(SettingsEffect.PerformHaptic(HapticType.LONG_PRESS))
|
||||||
showDebugCategory = true
|
showDebugCategory = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.meloda.fast.settings.model
|
||||||
|
|
||||||
|
sealed interface SettingsEffect {
|
||||||
|
data class Navigate(val intent: SettingsNavigationIntent) : SettingsEffect
|
||||||
|
data class PerformHaptic(val type: HapticType) : SettingsEffect
|
||||||
|
}
|
||||||
@@ -9,8 +9,6 @@ sealed class SettingsIntent {
|
|||||||
data class ItemLongClick(val key: String) : SettingsIntent()
|
data class ItemLongClick(val key: String) : SettingsIntent()
|
||||||
data class ItemValueChanged(val key: String, val newValue: Any?) : SettingsIntent()
|
data class ItemValueChanged(val key: String, val newValue: Any?) : SettingsIntent()
|
||||||
|
|
||||||
data object ConsumePerformHaptic : SettingsIntent()
|
|
||||||
|
|
||||||
sealed class Dialog : SettingsIntent() {
|
sealed class Dialog : SettingsIntent() {
|
||||||
data object Dismiss : Dialog()
|
data object Dismiss : Dialog()
|
||||||
data class ConfirmClick(val bundle: Bundle? = null) : Dialog()
|
data class ConfirmClick(val bundle: Bundle? = null) : Dialog()
|
||||||
|
|||||||
+17
-5
@@ -2,13 +2,18 @@ package dev.meloda.fast.settings.navigation
|
|||||||
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.settings.SettingsViewModel
|
import dev.meloda.fast.settings.SettingsViewModel
|
||||||
|
import dev.meloda.fast.settings.model.SettingsEffect
|
||||||
import dev.meloda.fast.settings.model.SettingsNavigationIntent
|
import dev.meloda.fast.settings.model.SettingsNavigationIntent
|
||||||
import dev.meloda.fast.settings.presentation.SettingsRoute
|
import dev.meloda.fast.settings.presentation.SettingsRoute
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@@ -19,19 +24,26 @@ fun NavGraphBuilder.settingsScreen(
|
|||||||
handleNavigationIntent: (SettingsNavigationIntent) -> Unit
|
handleNavigationIntent: (SettingsNavigationIntent) -> Unit
|
||||||
) {
|
) {
|
||||||
composable<Settings> {
|
composable<Settings> {
|
||||||
|
val view = LocalView.current
|
||||||
val viewModel: SettingsViewModel = koinViewModel()
|
val viewModel: SettingsViewModel = koinViewModel()
|
||||||
val screenState by viewModel.screenStateFlow.collectAsStateWithLifecycle()
|
val screenState by viewModel.screenStateFlow.collectAsStateWithLifecycle()
|
||||||
val hapticType by viewModel.hapticTypeFlow.collectAsStateWithLifecycle()
|
|
||||||
val navigationIntent by viewModel.navigationIntentFlow.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
LaunchedEffect(navigationIntent) {
|
LaunchedEffect(Unit) {
|
||||||
navigationIntent?.let(handleNavigationIntent)
|
viewModel.screenEffectFlow.onEach { effect ->
|
||||||
|
when (effect) {
|
||||||
|
is SettingsEffect.Navigate -> handleNavigationIntent(effect.intent)
|
||||||
|
is SettingsEffect.PerformHaptic -> {
|
||||||
|
if (AppSettings.General.enableHaptic) {
|
||||||
|
view.performHapticFeedback(effect.type.getHaptic())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsRoute(
|
SettingsRoute(
|
||||||
handleIntent = viewModel::handleIntent,
|
handleIntent = viewModel::handleIntent,
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
hapticType = hapticType
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-3
@@ -1,7 +1,6 @@
|
|||||||
package dev.meloda.fast.settings.presentation
|
package dev.meloda.fast.settings.presentation
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import dev.meloda.fast.settings.model.HapticType
|
|
||||||
import dev.meloda.fast.settings.model.SettingsIntent
|
import dev.meloda.fast.settings.model.SettingsIntent
|
||||||
import dev.meloda.fast.settings.model.SettingsScreenState
|
import dev.meloda.fast.settings.model.SettingsScreenState
|
||||||
|
|
||||||
@@ -9,12 +8,10 @@ import dev.meloda.fast.settings.model.SettingsScreenState
|
|||||||
fun SettingsRoute(
|
fun SettingsRoute(
|
||||||
handleIntent: (SettingsIntent) -> Unit,
|
handleIntent: (SettingsIntent) -> Unit,
|
||||||
screenState: SettingsScreenState,
|
screenState: SettingsScreenState,
|
||||||
hapticType: HapticType?
|
|
||||||
) {
|
) {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
handleIntent = handleIntent,
|
handleIntent = handleIntent,
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
hapticType = hapticType
|
|
||||||
)
|
)
|
||||||
|
|
||||||
HandleDialogs(
|
HandleDialogs(
|
||||||
|
|||||||
-16
@@ -21,12 +21,10 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -36,8 +34,6 @@ import dev.chrisbanes.haze.hazeEffect
|
|||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
|
||||||
import dev.meloda.fast.settings.model.HapticType
|
|
||||||
import dev.meloda.fast.settings.model.SettingsIntent
|
import dev.meloda.fast.settings.model.SettingsIntent
|
||||||
import dev.meloda.fast.settings.model.SettingsScreenState
|
import dev.meloda.fast.settings.model.SettingsScreenState
|
||||||
import dev.meloda.fast.settings.model.UiItem
|
import dev.meloda.fast.settings.model.UiItem
|
||||||
@@ -58,10 +54,7 @@ import dev.meloda.fast.ui.theme.LocalThemeConfig
|
|||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
handleIntent: (SettingsIntent) -> Unit,
|
handleIntent: (SettingsIntent) -> Unit,
|
||||||
screenState: SettingsScreenState = SettingsScreenState.EMPTY,
|
screenState: SettingsScreenState = SettingsScreenState.EMPTY,
|
||||||
hapticType: HapticType?
|
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
|
||||||
|
|
||||||
val onSettingsItemClicked by rememberUpdatedState { key: String ->
|
val onSettingsItemClicked by rememberUpdatedState { key: String ->
|
||||||
handleIntent(SettingsIntent.ItemClick(key))
|
handleIntent(SettingsIntent.ItemClick(key))
|
||||||
}
|
}
|
||||||
@@ -74,15 +67,6 @@ fun SettingsScreen(
|
|||||||
handleIntent(SettingsIntent.ItemValueChanged(key, newValue))
|
handleIntent(SettingsIntent.ItemValueChanged(key, newValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(hapticType) {
|
|
||||||
if (hapticType != null) {
|
|
||||||
if (AppSettings.General.enableHaptic) {
|
|
||||||
view.performHapticFeedback(hapticType.getHaptic())
|
|
||||||
}
|
|
||||||
handleIntent(SettingsIntent.ConsumePerformHaptic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val themeConfig = LocalThemeConfig.current
|
val themeConfig = LocalThemeConfig.current
|
||||||
|
|
||||||
val hazeState = remember { HazeState(true) }
|
val hazeState = remember { HazeState(true) }
|
||||||
|
|||||||
Reference in New Issue
Block a user