diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt index 55b8a12..8d28d8c 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt @@ -61,4 +61,4 @@ data class SpiderPage( ) @JvmInline -value class Route(val value: String) \ No newline at end of file +value class Route(val value: String) \ No newline at end of file diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/page/PageReducer.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/page/PageReducer.kt index 4289061..f33464a 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/page/PageReducer.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/page/PageReducer.kt @@ -1,10 +1,8 @@ package app.dapk.st.core.page import app.dapk.st.design.components.SpiderPage -import app.dapk.state.Action -import app.dapk.state.ReducerFactory -import app.dapk.state.change -import app.dapk.state.createReducer +import app.dapk.state.* +import kotlin.reflect.KClass fun

createPageReducer( initialPage: SpiderPage @@ -30,9 +28,9 @@ fun

createPageReducer( ) } -sealed interface PageAction : Action { - data class GoTo

(val page: SpiderPage

) : PageAction - data class UpdatePage

(val pageContent: P) : PageAction +sealed interface PageAction

: Action { + data class GoTo

(val page: SpiderPage

) : PageAction

+ data class UpdatePage

(val pageContent: P) : PageAction

} data class PageContainer

( @@ -43,3 +41,43 @@ fun PageContainer<*>.isDifferentPage(page: SpiderPage<*>): Boolean { return page::class != this.page::class } +interface PageReducerScope { + fun withPageContent(page: KClass, block: PageDispatchScope.() -> Unit) +} + +interface PageDispatchScope

{ + fun ReducerScope<*>.pageDispatch(action: PageAction

) + fun getPageState(): P? +} + +fun

createPageReducer( + initialPage: SpiderPage, + factory: PageReducerScope.() -> ReducerFactory, +): ReducerFactory, S>> = shareState { + combineReducers( + createPageReducer(initialPage), + factory(object : PageReducerScope { + override fun withPageContent(page: KClass, block: PageDispatchScope.() -> Unit) { + val currentPage = getSharedState().state1.page.state + if (currentPage::class == page) { + val pageDispatchScope = object : PageDispatchScope { + override fun ReducerScope<*>.pageDispatch(action: PageAction) { + 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 PageReducerScope.withPageContext(crossinline block: PageDispatchScope.(PC) -> Unit) { + withPageContent(PC::class) { getPageState()?.let { block(it) } } +} + diff --git a/domains/state/src/main/kotlin/app/dapk/state/State.kt b/domains/state/src/main/kotlin/app/dapk/state/State.kt index 0235af6..3914aaf 100644 --- a/domains/state/src/main/kotlin/app/dapk/state/State.kt +++ b/domains/state/src/main/kotlin/app/dapk/state/State.kt @@ -40,7 +40,7 @@ fun interface Reducer { private fun createScope(coroutineScope: CoroutineScope, store: Store) = object : ReducerScope { 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() } @@ -52,7 +52,7 @@ interface Store { interface ReducerScope { val coroutineScope: CoroutineScope - suspend fun dispatch(action: Action) + fun dispatch(action: Action) fun getState(): S } @@ -85,13 +85,13 @@ fun combineReducers(r1: ReducerFactory, r2: ReducerFactory): Re override fun create(scope: ReducerScope>): Reducer> { val r1Scope = object : ReducerScope { 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 } val r2Scope = object : ReducerScope { 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 } @@ -174,9 +174,10 @@ fun async(klass: KClass, block: suspend ReducerScope.(A) - fun multi(klass: KClass, block: Multi.(A) -> (ReducerScope) -> ActionHandler): (ReducerScope) -> ActionHandler { val multiScope = object : Multi { - override fun sideEffect(block: (A, S) -> Unit): (ReducerScope) -> ActionHandler = sideEffect(klass, block) + override fun sideEffect(block: suspend (S) -> Unit): (ReducerScope) -> ActionHandler = sideEffect(klass) { _, state -> block(state) } override fun change(block: (A, S) -> S): (ReducerScope) -> ActionHandler = change(klass, block) override fun async(block: suspend ReducerScope.(A) -> Unit): (ReducerScope) -> ActionHandler = async(klass, block) + override fun nothing() = sideEffect { } } return { @@ -187,7 +188,8 @@ fun multi(klass: KClass, block: Multi.(A) -> (ReducerSc } interface Multi { - fun sideEffect(block: (A, S) -> Unit): (ReducerScope) -> ActionHandler + fun sideEffect(block: suspend (S) -> Unit): (ReducerScope) -> ActionHandler + fun nothing(): (ReducerScope) -> ActionHandler fun change(block: (A, S) -> S): (ReducerScope) -> ActionHandler fun async(block: suspend ReducerScope.(A) -> Unit): (ReducerScope) -> ActionHandler } diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryReducer.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryReducer.kt index 41c8f8b..7ae45f8 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryReducer.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/state/DirectoryReducer.kt @@ -30,7 +30,7 @@ internal fun directoryReducer( }.launchIn(coroutineScope)) } - ComponentLifecycle.OnGone -> sideEffect { _, _ -> jobBag.cancel(KEY_SYNCING_JOB) } + ComponentLifecycle.OnGone -> sideEffect { jobBag.cancel(KEY_SYNCING_JOB) } } }, diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsActivity.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsActivity.kt index b6086f8..1cc26df 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsActivity.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsActivity.kt @@ -4,20 +4,17 @@ import android.os.Bundle import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.ui.Modifier -import app.dapk.st.core.DapkActivity -import app.dapk.st.core.module -import app.dapk.st.core.resetModules -import app.dapk.st.core.viewModel +import app.dapk.st.core.* class SettingsActivity : DapkActivity() { - private val settingsViewModel by viewModel { module().settingsViewModel() } + private val settingsState by state { module().settingsState() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Surface(Modifier.fillMaxSize()) { - SettingsScreen(settingsViewModel, onSignOut = { + SettingsScreen(settingsState, onSignOut = { resetModules() navigator.navigate.toHome() finish() diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt index b498f95..a3271e3 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt @@ -8,6 +8,8 @@ import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.engine.ChatEngine import app.dapk.st.push.PushModule import app.dapk.st.settings.eventlogger.EventLoggerViewModel +import app.dapk.st.settings.state.SettingsState +import app.dapk.st.settings.state.settingsReducer class SettingsModule( private val chatEngine: ChatEngine, @@ -22,20 +24,25 @@ class SettingsModule( private val messageOptionsStore: MessageOptionsStore, ) : ProvidableModule { - internal fun settingsViewModel(): SettingsViewModel { - return SettingsViewModel( - chatEngine, - storeModule.cacheCleaner(), - contentResolver, - UriFilenameResolver(contentResolver, coroutineDispatchers), - SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore, messageOptionsStore), - pushModule.pushTokenRegistrars(), - themeStore, - loggingStore, - messageOptionsStore, - ) + internal fun settingsState(): SettingsState { + return createStateViewModel { + settingsReducer( + chatEngine, + storeModule.cacheCleaner(), + contentResolver, + UriFilenameResolver(contentResolver, coroutineDispatchers), + SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore, messageOptionsStore), + pushModule.pushTokenRegistrars(), + themeStore, + loggingStore, + messageOptionsStore, + it, + JobBag(), + ) + } } + internal fun eventLogViewModel(): EventLoggerViewModel { return EventLoggerViewModel(storeModule.eventLogStore()) } diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt index 09522f2..6e73893 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt @@ -41,35 +41,44 @@ import app.dapk.st.core.components.CenteredLoading import app.dapk.st.core.components.Header import app.dapk.st.core.extensions.takeAs import app.dapk.st.core.getActivity +import app.dapk.st.core.page.PageAction import app.dapk.st.design.components.* import app.dapk.st.engine.ImportResult import app.dapk.st.navigator.Navigator import app.dapk.st.settings.SettingsEvent.* 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) @Composable -internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit, navigator: Navigator) { - viewModel.ObserveEvents(onSignOut) +internal fun SettingsScreen(settingsState: SettingsState, onSignOut: () -> Unit, navigator: Navigator) { + settingsState.ObserveEvents(onSignOut) LaunchedEffect(true) { - viewModel.start() + settingsState.dispatch(ComponentLifecycle.Visible) } val onNavigate: (SpiderPage?) -> Unit = { when (it) { 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) { - 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) { - Encryption(viewModel, it) + Encryption(settingsState, it) } item(Page.Routes.pushProviders) { - PushProviders(viewModel, it) + PushProviders(settingsState, it) } item(Page.Routes.importRoomKeys) { when (val result = it.importProgress) { @@ -83,7 +92,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit, ) { val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { it?.let { - viewModel.fileSelected(it) + settingsState.dispatch(RootActions.SelectKeysFile(it)) } } val keyboardController = LocalSoftwareKeyboardController.current @@ -100,7 +109,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit, var passwordVisibility by rememberSaveable { mutableStateOf(false) } val startImportAction = { keyboardController?.hide() - viewModel.importFromFileKeys(it.selectedFile.uri, passphrase) + settingsState.dispatch(RootActions.ImportKeysFromFile(it.selectedFile.uri, passphrase)) } TextField( @@ -235,40 +244,40 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit, onRetr } @Composable -private fun Encryption(viewModel: SettingsViewModel, page: Page.Security) { +private fun Encryption(state: SettingsState, page: Page.Security) { Column { - TextRow("Import room keys", includeDivider = false, onClick = { viewModel.goToImportRoom() }) + TextRow("Import room keys", includeDivider = false, onClick = { state.dispatch(ScreenAction.OpenImportRoom) }) } } @Composable -private fun PushProviders(viewModel: SettingsViewModel, state: Page.PushProviders) { +private fun PushProviders(state: SettingsState, page: Page.PushProviders) { LaunchedEffect(true) { - viewModel.fetchPushProviders() + state.dispatch(RootActions.FetchProviders) } - when (val lce = state.options) { + when (val lce = page.options) { null -> {} is Lce.Loading -> CenteredLoading() is Lce.Content -> { LazyColumn { items(lce.value) { 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) } } } } - is Lce.Error -> GenericError(cause = lce.cause) { viewModel.fetchPushProviders() } + is Lce.Error -> GenericError(cause = lce.cause) { state.dispatch(RootActions.FetchProviders) } } } @Composable -private fun SettingsViewModel.ObserveEvents(onSignOut: () -> Unit) { +private fun SettingsState.ObserveEvents(onSignOut: () -> Unit) { val context = LocalContext.current StartObserving { this@ObserveEvents.events.launch { diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsViewModel.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsViewModel.kt deleted file mode 100644 index b74daf8..0000000 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsViewModel.kt +++ /dev/null @@ -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 = defaultStateFactory(), -) : DapkViewModel( - 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) { - 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 { copy(options = Lce.Loading()) } - viewModelScope.launch { - val currentSelection = pushTokenRegistrars.currentSelection() - val options = pushTokenRegistrars.options() - updatePageState { - copy( - selection = currentSelection, - options = Lce.Content(options) - ) - } - } - } - - fun selectPushProvider(registrar: Registrar) { - viewModelScope.launch { - pushTokenRegistrars.makeSelection(registrar) - fetchPushProviders() - } - } - - fun importFromFileKeys(file: Uri, passphrase: String) { - updatePageState { copy(importProgress = ImportResult.Update(0)) } - viewModelScope.launch { - with(chatEngine) { - runCatching { contentResolver.openInputStream(file)!! } - .fold( - onSuccess = { fileStream -> - fileStream.importRoomKeys(passphrase) - .onEach { - updatePageState { copy(importProgress = it) } - } - .launchIn(viewModelScope) - }, - onFailure = { - updatePageState { 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 { copy(selectedFile = namedFile) } - } - } - - @Suppress("UNCHECKED_CAST") - private inline fun updatePageState(crossinline block: S.() -> S) { - val page = state.page - val currentState = page.state - require(currentState is S) - updateState { copy(page = (page as SpiderPage).copy(state = block(page.state))) } - } -} diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/state/SettingsAction.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/state/SettingsAction.kt new file mode 100644 index 0000000..d2b935a --- /dev/null +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/state/SettingsAction.kt @@ -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 +} diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/state/SettingsReducer.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/state/SettingsReducer.kt new file mode 100644 index 0000000..173aa5c --- /dev/null +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/state/SettingsReducer.kt @@ -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.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 { + pageDispatch(PageAction.UpdatePage(it.copy(options = Lce.Loading()))) + } + + val currentSelection = pushTokenRegistrars.currentSelection() + val options = pushTokenRegistrars.options() + withPageContext { + 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 { + 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 { + pageDispatch(PageAction.UpdatePage(it.copy(importProgress = progress))) + } + } + .launchIn(coroutineScope) + }, + onFailure = { + + withPageContext { + 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 { + 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, Unit>, SettingsEvent>