porting settings to reducer pattern
- creates a generate page reducer for combining with other reducers
This commit is contained in:
parent
8a8aa375c0
commit
7a13f530b0
|
@ -61,4 +61,4 @@ data class SpiderPage<T>(
|
||||||
)
|
)
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class Route<S>(val value: String)
|
value class Route<out S>(val value: String)
|
|
@ -1,10 +1,8 @@
|
||||||
package app.dapk.st.core.page
|
package app.dapk.st.core.page
|
||||||
|
|
||||||
import app.dapk.st.design.components.SpiderPage
|
import app.dapk.st.design.components.SpiderPage
|
||||||
import app.dapk.state.Action
|
import app.dapk.state.*
|
||||||
import app.dapk.state.ReducerFactory
|
import kotlin.reflect.KClass
|
||||||
import app.dapk.state.change
|
|
||||||
import app.dapk.state.createReducer
|
|
||||||
|
|
||||||
fun <P : Any> createPageReducer(
|
fun <P : Any> createPageReducer(
|
||||||
initialPage: SpiderPage<out P>
|
initialPage: SpiderPage<out P>
|
||||||
|
@ -30,9 +28,9 @@ fun <P : Any> createPageReducer(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface PageAction : Action {
|
sealed interface PageAction<P> : Action {
|
||||||
data class GoTo<P : Any>(val page: SpiderPage<P>) : PageAction
|
data class GoTo<P : Any>(val page: SpiderPage<P>) : PageAction<P>
|
||||||
data class UpdatePage<P : Any>(val pageContent: P) : PageAction
|
data class UpdatePage<P : Any>(val pageContent: P) : PageAction<P>
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PageContainer<P>(
|
data class PageContainer<P>(
|
||||||
|
@ -43,3 +41,43 @@ fun PageContainer<*>.isDifferentPage(page: SpiderPage<*>): Boolean {
|
||||||
return page::class != this.page::class
|
return page::class != this.page::class
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PageReducerScope {
|
||||||
|
fun <PC : Any> withPageContent(page: KClass<PC>, block: PageDispatchScope<PC>.() -> Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageDispatchScope<P> {
|
||||||
|
fun ReducerScope<*>.pageDispatch(action: PageAction<P>)
|
||||||
|
fun getPageState(): P?
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <P : Any, S : Any> createPageReducer(
|
||||||
|
initialPage: SpiderPage<out P>,
|
||||||
|
factory: PageReducerScope.() -> ReducerFactory<S>,
|
||||||
|
): ReducerFactory<Combined2<PageContainer<P>, S>> = shareState {
|
||||||
|
combineReducers(
|
||||||
|
createPageReducer(initialPage),
|
||||||
|
factory(object : PageReducerScope {
|
||||||
|
override fun <PC : Any> withPageContent(page: KClass<PC>, block: PageDispatchScope<PC>.() -> Unit) {
|
||||||
|
val currentPage = getSharedState().state1.page.state
|
||||||
|
if (currentPage::class == page) {
|
||||||
|
val pageDispatchScope = object : PageDispatchScope<PC> {
|
||||||
|
override fun ReducerScope<*>.pageDispatch(action: PageAction<PC>) {
|
||||||
|
val currentPageGuard = getSharedState().state1.page.state
|
||||||
|
if (currentPageGuard::class == page) {
|
||||||
|
dispatch(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPageState() = getSharedState().state1.page.state as? PC
|
||||||
|
}
|
||||||
|
block(pageDispatchScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified PC : Any> PageReducerScope.withPageContext(crossinline block: PageDispatchScope<PC>.(PC) -> Unit) {
|
||||||
|
withPageContent(PC::class) { getPageState()?.let { block(it) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ fun interface Reducer<S> {
|
||||||
|
|
||||||
private fun <S> createScope(coroutineScope: CoroutineScope, store: Store<S>) = object : ReducerScope<S> {
|
private fun <S> createScope(coroutineScope: CoroutineScope, store: Store<S>) = object : ReducerScope<S> {
|
||||||
override val coroutineScope = coroutineScope
|
override val coroutineScope = coroutineScope
|
||||||
override suspend fun dispatch(action: Action) = store.dispatch(action)
|
override fun dispatch(action: Action) = store.dispatch(action)
|
||||||
override fun getState(): S = store.getState()
|
override fun getState(): S = store.getState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ interface Store<S> {
|
||||||
|
|
||||||
interface ReducerScope<S> {
|
interface ReducerScope<S> {
|
||||||
val coroutineScope: CoroutineScope
|
val coroutineScope: CoroutineScope
|
||||||
suspend fun dispatch(action: Action)
|
fun dispatch(action: Action)
|
||||||
fun getState(): S
|
fun getState(): S
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,13 +85,13 @@ fun <S1, S2> combineReducers(r1: ReducerFactory<S1>, r2: ReducerFactory<S2>): Re
|
||||||
override fun create(scope: ReducerScope<Combined2<S1, S2>>): Reducer<Combined2<S1, S2>> {
|
override fun create(scope: ReducerScope<Combined2<S1, S2>>): Reducer<Combined2<S1, S2>> {
|
||||||
val r1Scope = object : ReducerScope<S1> {
|
val r1Scope = object : ReducerScope<S1> {
|
||||||
override val coroutineScope: CoroutineScope = scope.coroutineScope
|
override val coroutineScope: CoroutineScope = scope.coroutineScope
|
||||||
override suspend fun dispatch(action: Action) = scope.dispatch(action)
|
override fun dispatch(action: Action) = scope.dispatch(action)
|
||||||
override fun getState() = scope.getState().state1
|
override fun getState() = scope.getState().state1
|
||||||
}
|
}
|
||||||
|
|
||||||
val r2Scope = object : ReducerScope<S2> {
|
val r2Scope = object : ReducerScope<S2> {
|
||||||
override val coroutineScope: CoroutineScope = scope.coroutineScope
|
override val coroutineScope: CoroutineScope = scope.coroutineScope
|
||||||
override suspend fun dispatch(action: Action) = scope.dispatch(action)
|
override fun dispatch(action: Action) = scope.dispatch(action)
|
||||||
override fun getState() = scope.getState().state2
|
override fun getState() = scope.getState().state2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,9 +174,10 @@ fun <A : Action, S> async(klass: KClass<A>, block: suspend ReducerScope<S>.(A) -
|
||||||
|
|
||||||
fun <A : Action, S> multi(klass: KClass<A>, block: Multi<A, S>.(A) -> (ReducerScope<S>) -> ActionHandler<S>): (ReducerScope<S>) -> ActionHandler<S> {
|
fun <A : Action, S> multi(klass: KClass<A>, block: Multi<A, S>.(A) -> (ReducerScope<S>) -> ActionHandler<S>): (ReducerScope<S>) -> ActionHandler<S> {
|
||||||
val multiScope = object : Multi<A, S> {
|
val multiScope = object : Multi<A, S> {
|
||||||
override fun sideEffect(block: (A, S) -> Unit): (ReducerScope<S>) -> ActionHandler<S> = sideEffect(klass, block)
|
override fun sideEffect(block: suspend (S) -> Unit): (ReducerScope<S>) -> ActionHandler<S> = sideEffect(klass) { _, state -> block(state) }
|
||||||
override fun change(block: (A, S) -> S): (ReducerScope<S>) -> ActionHandler<S> = change(klass, block)
|
override fun change(block: (A, S) -> S): (ReducerScope<S>) -> ActionHandler<S> = change(klass, block)
|
||||||
override fun async(block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S> = async(klass, block)
|
override fun async(block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S> = async(klass, block)
|
||||||
|
override fun nothing() = sideEffect { }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -187,7 +188,8 @@ fun <A : Action, S> multi(klass: KClass<A>, block: Multi<A, S>.(A) -> (ReducerSc
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Multi<A : Action, S> {
|
interface Multi<A : Action, S> {
|
||||||
fun sideEffect(block: (A, S) -> Unit): (ReducerScope<S>) -> ActionHandler<S>
|
fun sideEffect(block: suspend (S) -> Unit): (ReducerScope<S>) -> ActionHandler<S>
|
||||||
|
fun nothing(): (ReducerScope<S>) -> ActionHandler<S>
|
||||||
fun change(block: (A, S) -> S): (ReducerScope<S>) -> ActionHandler<S>
|
fun change(block: (A, S) -> S): (ReducerScope<S>) -> ActionHandler<S>
|
||||||
fun async(block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S>
|
fun async(block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S>
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ internal fun directoryReducer(
|
||||||
}.launchIn(coroutineScope))
|
}.launchIn(coroutineScope))
|
||||||
}
|
}
|
||||||
|
|
||||||
ComponentLifecycle.OnGone -> sideEffect { _, _ -> jobBag.cancel(KEY_SYNCING_JOB) }
|
ComponentLifecycle.OnGone -> sideEffect { jobBag.cancel(KEY_SYNCING_JOB) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -4,20 +4,17 @@ import android.os.Bundle
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import app.dapk.st.core.DapkActivity
|
import app.dapk.st.core.*
|
||||||
import app.dapk.st.core.module
|
|
||||||
import app.dapk.st.core.resetModules
|
|
||||||
import app.dapk.st.core.viewModel
|
|
||||||
|
|
||||||
class SettingsActivity : DapkActivity() {
|
class SettingsActivity : DapkActivity() {
|
||||||
|
|
||||||
private val settingsViewModel by viewModel { module<SettingsModule>().settingsViewModel() }
|
private val settingsState by state { module<SettingsModule>().settingsState() }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
Surface(Modifier.fillMaxSize()) {
|
Surface(Modifier.fillMaxSize()) {
|
||||||
SettingsScreen(settingsViewModel, onSignOut = {
|
SettingsScreen(settingsState, onSignOut = {
|
||||||
resetModules()
|
resetModules()
|
||||||
navigator.navigate.toHome()
|
navigator.navigate.toHome()
|
||||||
finish()
|
finish()
|
||||||
|
|
|
@ -8,6 +8,8 @@ import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
import app.dapk.st.engine.ChatEngine
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.push.PushModule
|
import app.dapk.st.push.PushModule
|
||||||
import app.dapk.st.settings.eventlogger.EventLoggerViewModel
|
import app.dapk.st.settings.eventlogger.EventLoggerViewModel
|
||||||
|
import app.dapk.st.settings.state.SettingsState
|
||||||
|
import app.dapk.st.settings.state.settingsReducer
|
||||||
|
|
||||||
class SettingsModule(
|
class SettingsModule(
|
||||||
private val chatEngine: ChatEngine,
|
private val chatEngine: ChatEngine,
|
||||||
|
@ -22,20 +24,25 @@ class SettingsModule(
|
||||||
private val messageOptionsStore: MessageOptionsStore,
|
private val messageOptionsStore: MessageOptionsStore,
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
internal fun settingsViewModel(): SettingsViewModel {
|
internal fun settingsState(): SettingsState {
|
||||||
return SettingsViewModel(
|
return createStateViewModel {
|
||||||
chatEngine,
|
settingsReducer(
|
||||||
storeModule.cacheCleaner(),
|
chatEngine,
|
||||||
contentResolver,
|
storeModule.cacheCleaner(),
|
||||||
UriFilenameResolver(contentResolver, coroutineDispatchers),
|
contentResolver,
|
||||||
SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore, messageOptionsStore),
|
UriFilenameResolver(contentResolver, coroutineDispatchers),
|
||||||
pushModule.pushTokenRegistrars(),
|
SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore, messageOptionsStore),
|
||||||
themeStore,
|
pushModule.pushTokenRegistrars(),
|
||||||
loggingStore,
|
themeStore,
|
||||||
messageOptionsStore,
|
loggingStore,
|
||||||
)
|
messageOptionsStore,
|
||||||
|
it,
|
||||||
|
JobBag(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal fun eventLogViewModel(): EventLoggerViewModel {
|
internal fun eventLogViewModel(): EventLoggerViewModel {
|
||||||
return EventLoggerViewModel(storeModule.eventLogStore())
|
return EventLoggerViewModel(storeModule.eventLogStore())
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,35 +41,44 @@ import app.dapk.st.core.components.CenteredLoading
|
||||||
import app.dapk.st.core.components.Header
|
import app.dapk.st.core.components.Header
|
||||||
import app.dapk.st.core.extensions.takeAs
|
import app.dapk.st.core.extensions.takeAs
|
||||||
import app.dapk.st.core.getActivity
|
import app.dapk.st.core.getActivity
|
||||||
|
import app.dapk.st.core.page.PageAction
|
||||||
import app.dapk.st.design.components.*
|
import app.dapk.st.design.components.*
|
||||||
import app.dapk.st.engine.ImportResult
|
import app.dapk.st.engine.ImportResult
|
||||||
import app.dapk.st.navigator.Navigator
|
import app.dapk.st.navigator.Navigator
|
||||||
import app.dapk.st.settings.SettingsEvent.*
|
import app.dapk.st.settings.SettingsEvent.*
|
||||||
import app.dapk.st.settings.eventlogger.EventLogActivity
|
import app.dapk.st.settings.eventlogger.EventLogActivity
|
||||||
|
import app.dapk.st.settings.state.ComponentLifecycle
|
||||||
|
import app.dapk.st.settings.state.RootActions
|
||||||
|
import app.dapk.st.settings.state.ScreenAction
|
||||||
|
import app.dapk.st.settings.state.SettingsState
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit, navigator: Navigator) {
|
internal fun SettingsScreen(settingsState: SettingsState, onSignOut: () -> Unit, navigator: Navigator) {
|
||||||
viewModel.ObserveEvents(onSignOut)
|
settingsState.ObserveEvents(onSignOut)
|
||||||
LaunchedEffect(true) {
|
LaunchedEffect(true) {
|
||||||
viewModel.start()
|
settingsState.dispatch(ComponentLifecycle.Visible)
|
||||||
}
|
}
|
||||||
|
|
||||||
val onNavigate: (SpiderPage<out Page>?) -> Unit = {
|
val onNavigate: (SpiderPage<out Page>?) -> Unit = {
|
||||||
when (it) {
|
when (it) {
|
||||||
null -> navigator.navigate.upToHome()
|
null -> navigator.navigate.upToHome()
|
||||||
else -> viewModel.goTo(it)
|
else -> settingsState.dispatch(PageAction.GoTo(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) {
|
Spider(currentPage = settingsState.current.state1.page, onNavigate = onNavigate) {
|
||||||
item(Page.Routes.root) {
|
item(Page.Routes.root) {
|
||||||
RootSettings(it, onClick = { viewModel.onClick(it) }, onRetry = { viewModel.start() })
|
RootSettings(
|
||||||
|
it,
|
||||||
|
onClick = { settingsState.dispatch(ScreenAction.OnClick(it)) },
|
||||||
|
onRetry = { settingsState.dispatch(ComponentLifecycle.Visible) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
item(Page.Routes.encryption) {
|
item(Page.Routes.encryption) {
|
||||||
Encryption(viewModel, it)
|
Encryption(settingsState, it)
|
||||||
}
|
}
|
||||||
item(Page.Routes.pushProviders) {
|
item(Page.Routes.pushProviders) {
|
||||||
PushProviders(viewModel, it)
|
PushProviders(settingsState, it)
|
||||||
}
|
}
|
||||||
item(Page.Routes.importRoomKeys) {
|
item(Page.Routes.importRoomKeys) {
|
||||||
when (val result = it.importProgress) {
|
when (val result = it.importProgress) {
|
||||||
|
@ -83,7 +92,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
|
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
|
||||||
it?.let {
|
it?.let {
|
||||||
viewModel.fileSelected(it)
|
settingsState.dispatch(RootActions.SelectKeysFile(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
@ -100,7 +109,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
|
||||||
var passwordVisibility by rememberSaveable { mutableStateOf(false) }
|
var passwordVisibility by rememberSaveable { mutableStateOf(false) }
|
||||||
val startImportAction = {
|
val startImportAction = {
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
viewModel.importFromFileKeys(it.selectedFile.uri, passphrase)
|
settingsState.dispatch(RootActions.ImportKeysFromFile(it.selectedFile.uri, passphrase))
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
|
@ -235,40 +244,40 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit, onRetr
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Encryption(viewModel: SettingsViewModel, page: Page.Security) {
|
private fun Encryption(state: SettingsState, page: Page.Security) {
|
||||||
Column {
|
Column {
|
||||||
TextRow("Import room keys", includeDivider = false, onClick = { viewModel.goToImportRoom() })
|
TextRow("Import room keys", includeDivider = false, onClick = { state.dispatch(ScreenAction.OpenImportRoom) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PushProviders(viewModel: SettingsViewModel, state: Page.PushProviders) {
|
private fun PushProviders(state: SettingsState, page: Page.PushProviders) {
|
||||||
LaunchedEffect(true) {
|
LaunchedEffect(true) {
|
||||||
viewModel.fetchPushProviders()
|
state.dispatch(RootActions.FetchProviders)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (val lce = state.options) {
|
when (val lce = page.options) {
|
||||||
null -> {}
|
null -> {}
|
||||||
is Lce.Loading -> CenteredLoading()
|
is Lce.Loading -> CenteredLoading()
|
||||||
is Lce.Content -> {
|
is Lce.Content -> {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(lce.value) {
|
items(lce.value) {
|
||||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
RadioButton(selected = it == state.selection, onClick = { viewModel.selectPushProvider(it) })
|
RadioButton(selected = it == page.selection, onClick = { state.dispatch(RootActions.SelectPushProvider(it)) })
|
||||||
Text(it.id)
|
Text(it.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Lce.Error -> GenericError(cause = lce.cause) { viewModel.fetchPushProviders() }
|
is Lce.Error -> GenericError(cause = lce.cause) { state.dispatch(RootActions.FetchProviders) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsViewModel.ObserveEvents(onSignOut: () -> Unit) {
|
private fun SettingsState.ObserveEvents(onSignOut: () -> Unit) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
StartObserving {
|
StartObserving {
|
||||||
this@ObserveEvents.events.launch {
|
this@ObserveEvents.events.launch {
|
||||||
|
|
|
@ -1,182 +0,0 @@
|
||||||
package app.dapk.st.settings
|
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import app.dapk.st.core.Lce
|
|
||||||
import app.dapk.st.core.ThemeStore
|
|
||||||
import app.dapk.st.design.components.SpiderPage
|
|
||||||
import app.dapk.st.domain.StoreCleaner
|
|
||||||
import app.dapk.st.domain.application.eventlog.LoggingStore
|
|
||||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
|
||||||
import app.dapk.st.engine.ChatEngine
|
|
||||||
import app.dapk.st.engine.ImportResult
|
|
||||||
import app.dapk.st.push.PushTokenRegistrars
|
|
||||||
import app.dapk.st.push.Registrar
|
|
||||||
import app.dapk.st.settings.SettingItem.Id.*
|
|
||||||
import app.dapk.st.settings.SettingsEvent.*
|
|
||||||
import app.dapk.st.viewmodel.DapkViewModel
|
|
||||||
import app.dapk.st.viewmodel.MutableStateFactory
|
|
||||||
import app.dapk.st.viewmodel.defaultStateFactory
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
private const val PRIVACY_POLICY_URL = "https://ouchadam.github.io/small-talk/privacy/"
|
|
||||||
|
|
||||||
internal class SettingsViewModel(
|
|
||||||
private val chatEngine: ChatEngine,
|
|
||||||
private val cacheCleaner: StoreCleaner,
|
|
||||||
private val contentResolver: ContentResolver,
|
|
||||||
private val uriFilenameResolver: UriFilenameResolver,
|
|
||||||
private val settingsItemFactory: SettingsItemFactory,
|
|
||||||
private val pushTokenRegistrars: PushTokenRegistrars,
|
|
||||||
private val themeStore: ThemeStore,
|
|
||||||
private val loggingStore: LoggingStore,
|
|
||||||
private val messageOptionsStore: MessageOptionsStore,
|
|
||||||
factory: MutableStateFactory<SettingsScreenState> = defaultStateFactory(),
|
|
||||||
) : DapkViewModel<SettingsScreenState, SettingsEvent>(
|
|
||||||
initialState = SettingsScreenState(SpiderPage(Page.Routes.root, "Settings", null, Page.Root(Lce.Loading()))),
|
|
||||||
factory = factory,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun start() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
val root = Page.Root(Lce.Content(settingsItemFactory.root()))
|
|
||||||
val rootPage = SpiderPage(Page.Routes.root, "Settings", null, root)
|
|
||||||
updateState { copy(page = rootPage) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun goTo(page: SpiderPage<out Page>) {
|
|
||||||
updateState { copy(page = page) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onClick(item: SettingItem) {
|
|
||||||
when (item.id) {
|
|
||||||
SignOut -> viewModelScope.launch {
|
|
||||||
cacheCleaner.cleanCache(removeCredentials = true)
|
|
||||||
_events.emit(SignedOut)
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessToken -> viewModelScope.launch {
|
|
||||||
require(item is SettingItem.AccessToken)
|
|
||||||
_events.emit(CopyToClipboard("Token copied", item.accessToken))
|
|
||||||
}
|
|
||||||
|
|
||||||
ClearCache -> viewModelScope.launch {
|
|
||||||
cacheCleaner.cleanCache(removeCredentials = false)
|
|
||||||
_events.emit(Toast(message = "Cache deleted"))
|
|
||||||
}
|
|
||||||
|
|
||||||
EventLog -> viewModelScope.launch {
|
|
||||||
_events.emit(OpenEventLog)
|
|
||||||
}
|
|
||||||
|
|
||||||
Encryption -> {
|
|
||||||
updateState {
|
|
||||||
copy(page = SpiderPage(Page.Routes.encryption, "Encryption", Page.Routes.root, Page.Security))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PrivacyPolicy -> viewModelScope.launch {
|
|
||||||
_events.emit(OpenUrl(PRIVACY_POLICY_URL))
|
|
||||||
}
|
|
||||||
|
|
||||||
PushProvider -> {
|
|
||||||
updateState {
|
|
||||||
copy(page = SpiderPage(Page.Routes.pushProviders, "Push providers", Page.Routes.root, Page.PushProviders()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ignored -> {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleDynamicTheme -> viewModelScope.launch {
|
|
||||||
themeStore.storeMaterialYouEnabled(!themeStore.isMaterialYouEnabled())
|
|
||||||
refreshRoot()
|
|
||||||
_events.emit(RecreateActivity)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleEnableLogs -> viewModelScope.launch {
|
|
||||||
loggingStore.setEnabled(!loggingStore.isEnabled())
|
|
||||||
refreshRoot()
|
|
||||||
}
|
|
||||||
|
|
||||||
ToggleSendReadReceipts -> viewModelScope.launch {
|
|
||||||
messageOptionsStore.setReadReceiptsDisabled(!messageOptionsStore.isReadReceiptsDisabled())
|
|
||||||
refreshRoot()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshRoot() {
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchPushProviders() {
|
|
||||||
updatePageState<Page.PushProviders> { copy(options = Lce.Loading()) }
|
|
||||||
viewModelScope.launch {
|
|
||||||
val currentSelection = pushTokenRegistrars.currentSelection()
|
|
||||||
val options = pushTokenRegistrars.options()
|
|
||||||
updatePageState<Page.PushProviders> {
|
|
||||||
copy(
|
|
||||||
selection = currentSelection,
|
|
||||||
options = Lce.Content(options)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun selectPushProvider(registrar: Registrar) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
pushTokenRegistrars.makeSelection(registrar)
|
|
||||||
fetchPushProviders()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun importFromFileKeys(file: Uri, passphrase: String) {
|
|
||||||
updatePageState<Page.ImportRoomKey> { copy(importProgress = ImportResult.Update(0)) }
|
|
||||||
viewModelScope.launch {
|
|
||||||
with(chatEngine) {
|
|
||||||
runCatching { contentResolver.openInputStream(file)!! }
|
|
||||||
.fold(
|
|
||||||
onSuccess = { fileStream ->
|
|
||||||
fileStream.importRoomKeys(passphrase)
|
|
||||||
.onEach {
|
|
||||||
updatePageState<Page.ImportRoomKey> { copy(importProgress = it) }
|
|
||||||
}
|
|
||||||
.launchIn(viewModelScope)
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
updatePageState<Page.ImportRoomKey> { copy(importProgress = ImportResult.Error(ImportResult.Error.Type.UnableToOpenFile)) }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun goToImportRoom() {
|
|
||||||
goTo(SpiderPage(Page.Routes.importRoomKeys, "Import room keys", Page.Routes.encryption, Page.ImportRoomKey()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fileSelected(file: Uri) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
val namedFile = NamedUri(
|
|
||||||
name = uriFilenameResolver.readFilenameFromUri(file),
|
|
||||||
uri = file
|
|
||||||
)
|
|
||||||
updatePageState<Page.ImportRoomKey> { copy(selectedFile = namedFile) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
private inline fun <reified S : Page> updatePageState(crossinline block: S.() -> S) {
|
|
||||||
val page = state.page
|
|
||||||
val currentState = page.state
|
|
||||||
require(currentState is S)
|
|
||||||
updateState { copy(page = (page as SpiderPage<S>).copy(state = block(page.state))) }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package app.dapk.st.settings.state
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import app.dapk.st.push.Registrar
|
||||||
|
import app.dapk.st.settings.SettingItem
|
||||||
|
import app.dapk.state.Action
|
||||||
|
|
||||||
|
internal sealed interface ScreenAction : Action {
|
||||||
|
data class OnClick(val item: SettingItem) : ScreenAction
|
||||||
|
object OpenImportRoom : ScreenAction
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed interface RootActions : Action {
|
||||||
|
object FetchProviders : RootActions
|
||||||
|
data class SelectPushProvider(val registrar: Registrar) : RootActions
|
||||||
|
data class ImportKeysFromFile(val file: Uri, val passphrase: String) : RootActions
|
||||||
|
data class SelectKeysFile(val file: Uri) : RootActions
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed interface ComponentLifecycle : Action {
|
||||||
|
object Visible : ComponentLifecycle
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
package app.dapk.st.settings.state
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import app.dapk.st.core.JobBag
|
||||||
|
import app.dapk.st.core.Lce
|
||||||
|
import app.dapk.st.core.State
|
||||||
|
import app.dapk.st.core.ThemeStore
|
||||||
|
import app.dapk.st.core.page.PageAction
|
||||||
|
import app.dapk.st.core.page.PageContainer
|
||||||
|
import app.dapk.st.core.page.createPageReducer
|
||||||
|
import app.dapk.st.core.page.withPageContext
|
||||||
|
import app.dapk.st.design.components.SpiderPage
|
||||||
|
import app.dapk.st.domain.StoreCleaner
|
||||||
|
import app.dapk.st.domain.application.eventlog.LoggingStore
|
||||||
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
|
import app.dapk.st.engine.ChatEngine
|
||||||
|
import app.dapk.st.engine.ImportResult
|
||||||
|
import app.dapk.st.push.PushTokenRegistrars
|
||||||
|
import app.dapk.st.settings.*
|
||||||
|
import app.dapk.st.settings.SettingItem.Id.*
|
||||||
|
import app.dapk.st.settings.SettingsEvent.*
|
||||||
|
import app.dapk.state.Combined2
|
||||||
|
import app.dapk.state.async
|
||||||
|
import app.dapk.state.createReducer
|
||||||
|
import app.dapk.state.multi
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private const val PRIVACY_POLICY_URL = "https://ouchadam.github.io/small-talk/privacy/"
|
||||||
|
|
||||||
|
internal fun settingsReducer(
|
||||||
|
chatEngine: ChatEngine,
|
||||||
|
cacheCleaner: StoreCleaner,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
uriFilenameResolver: UriFilenameResolver,
|
||||||
|
settingsItemFactory: SettingsItemFactory,
|
||||||
|
pushTokenRegistrars: PushTokenRegistrars,
|
||||||
|
themeStore: ThemeStore,
|
||||||
|
loggingStore: LoggingStore,
|
||||||
|
messageOptionsStore: MessageOptionsStore,
|
||||||
|
eventEmitter: suspend (SettingsEvent) -> Unit,
|
||||||
|
jobBag: JobBag,
|
||||||
|
) = createPageReducer(
|
||||||
|
initialPage = SpiderPage<Page>(Page.Routes.root, "Settings", null, Page.Root(Lce.Loading())),
|
||||||
|
factory = {
|
||||||
|
createReducer(
|
||||||
|
initialState = Unit,
|
||||||
|
|
||||||
|
async(ComponentLifecycle.Visible::class) {
|
||||||
|
jobBag.replace("page", coroutineScope.launch {
|
||||||
|
val root = Page.Root(Lce.Content(settingsItemFactory.root()))
|
||||||
|
val rootPage = SpiderPage(Page.Routes.root, "Settings", null, root)
|
||||||
|
dispatch(PageAction.GoTo(rootPage))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async(RootActions.FetchProviders::class) {
|
||||||
|
withPageContext<Page.PushProviders> {
|
||||||
|
pageDispatch(PageAction.UpdatePage(it.copy(options = Lce.Loading())))
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentSelection = pushTokenRegistrars.currentSelection()
|
||||||
|
val options = pushTokenRegistrars.options()
|
||||||
|
withPageContext<Page.PushProviders> {
|
||||||
|
pageDispatch(
|
||||||
|
PageAction.UpdatePage(
|
||||||
|
it.copy(
|
||||||
|
selection = currentSelection,
|
||||||
|
options = Lce.Content(options)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async(RootActions.SelectPushProvider::class) {
|
||||||
|
pushTokenRegistrars.makeSelection(it.registrar)
|
||||||
|
dispatch(RootActions.FetchProviders)
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
async(RootActions.ImportKeysFromFile::class) { action ->
|
||||||
|
withPageContext<Page.ImportRoomKey> {
|
||||||
|
pageDispatch(PageAction.UpdatePage(it.copy(importProgress = ImportResult.Update(0))))
|
||||||
|
}
|
||||||
|
|
||||||
|
with(chatEngine) {
|
||||||
|
runCatching { contentResolver.openInputStream(action.file)!! }
|
||||||
|
.fold(
|
||||||
|
onSuccess = { fileStream ->
|
||||||
|
fileStream.importRoomKeys(action.passphrase)
|
||||||
|
.onEach { progress ->
|
||||||
|
withPageContext<Page.ImportRoomKey> {
|
||||||
|
pageDispatch(PageAction.UpdatePage(it.copy(importProgress = progress)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(coroutineScope)
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
|
||||||
|
withPageContext<Page.ImportRoomKey> {
|
||||||
|
pageDispatch(PageAction.UpdatePage(it.copy(importProgress = ImportResult.Error(ImportResult.Error.Type.UnableToOpenFile))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async(RootActions.SelectKeysFile::class) { action ->
|
||||||
|
val namedFile = NamedUri(
|
||||||
|
name = uriFilenameResolver.readFilenameFromUri(action.file),
|
||||||
|
uri = action.file
|
||||||
|
)
|
||||||
|
|
||||||
|
withPageContext<Page.ImportRoomKey> {
|
||||||
|
pageDispatch(PageAction.UpdatePage(it.copy(selectedFile = namedFile)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
multi(ScreenAction.OnClick::class) { action ->
|
||||||
|
val item = action.item
|
||||||
|
when (item.id) {
|
||||||
|
SignOut -> sideEffect {
|
||||||
|
cacheCleaner.cleanCache(removeCredentials = true)
|
||||||
|
eventEmitter.invoke(SignedOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessToken -> sideEffect {
|
||||||
|
require(item is SettingItem.AccessToken)
|
||||||
|
eventEmitter.invoke(CopyToClipboard("Token copied", item.accessToken))
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearCache -> sideEffect {
|
||||||
|
cacheCleaner.cleanCache(removeCredentials = false)
|
||||||
|
eventEmitter.invoke(Toast(message = "Cache deleted"))
|
||||||
|
}
|
||||||
|
|
||||||
|
EventLog -> sideEffect {
|
||||||
|
eventEmitter.invoke(OpenEventLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
Encryption -> async {
|
||||||
|
dispatch(PageAction.GoTo(SpiderPage(Page.Routes.encryption, "Encryption", Page.Routes.root, Page.Security)))
|
||||||
|
}
|
||||||
|
|
||||||
|
PrivacyPolicy -> sideEffect {
|
||||||
|
eventEmitter.invoke(OpenUrl(PRIVACY_POLICY_URL))
|
||||||
|
}
|
||||||
|
|
||||||
|
PushProvider -> async {
|
||||||
|
dispatch(PageAction.GoTo(SpiderPage(Page.Routes.pushProviders, "Push providers", Page.Routes.root, Page.PushProviders())))
|
||||||
|
}
|
||||||
|
|
||||||
|
Ignored -> {
|
||||||
|
nothing()
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleDynamicTheme -> async {
|
||||||
|
themeStore.storeMaterialYouEnabled(!themeStore.isMaterialYouEnabled())
|
||||||
|
dispatch(ComponentLifecycle.Visible)
|
||||||
|
eventEmitter.invoke(RecreateActivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleEnableLogs -> async {
|
||||||
|
loggingStore.setEnabled(!loggingStore.isEnabled())
|
||||||
|
dispatch(ComponentLifecycle.Visible)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleSendReadReceipts -> async {
|
||||||
|
messageOptionsStore.setReadReceiptsDisabled(!messageOptionsStore.isReadReceiptsDisabled())
|
||||||
|
dispatch(ComponentLifecycle.Visible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async(ScreenAction.OpenImportRoom::class) {
|
||||||
|
dispatch(PageAction.GoTo(SpiderPage(Page.Routes.importRoomKeys, "Import room keys", Page.Routes.encryption, Page.ImportRoomKey())))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
internal typealias SettingsState = State<Combined2<PageContainer<Page>, Unit>, SettingsEvent>
|
Loading…
Reference in New Issue