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
|
||||
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
|
||||
|
||||
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 <P : Any> createPageReducer(
|
||||
initialPage: SpiderPage<out P>
|
||||
|
@ -30,9 +28,9 @@ fun <P : Any> createPageReducer(
|
|||
)
|
||||
}
|
||||
|
||||
sealed interface PageAction : Action {
|
||||
data class GoTo<P : Any>(val page: SpiderPage<P>) : PageAction
|
||||
data class UpdatePage<P : Any>(val pageContent: P) : PageAction
|
||||
sealed interface PageAction<P> : Action {
|
||||
data class GoTo<P : Any>(val page: SpiderPage<P>) : PageAction<P>
|
||||
data class UpdatePage<P : Any>(val pageContent: P) : PageAction<P>
|
||||
}
|
||||
|
||||
data class PageContainer<P>(
|
||||
|
@ -43,3 +41,43 @@ fun PageContainer<*>.isDifferentPage(page: SpiderPage<*>): Boolean {
|
|||
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> {
|
||||
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<S> {
|
|||
|
||||
interface ReducerScope<S> {
|
||||
val coroutineScope: CoroutineScope
|
||||
suspend fun dispatch(action: Action)
|
||||
fun dispatch(action: Action)
|
||||
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>> {
|
||||
val r1Scope = object : ReducerScope<S1> {
|
||||
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<S2> {
|
||||
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 <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> {
|
||||
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 async(block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S> = async(klass, block)
|
||||
override fun nothing() = sideEffect { }
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -187,7 +188,8 @@ fun <A : Action, S> multi(klass: KClass<A>, block: Multi<A, S>.(A) -> (ReducerSc
|
|||
}
|
||||
|
||||
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 async(block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S>
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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<SettingsModule>().settingsViewModel() }
|
||||
private val settingsState by state { module<SettingsModule>().settingsState() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
SettingsScreen(settingsViewModel, onSignOut = {
|
||||
SettingsScreen(settingsState, onSignOut = {
|
||||
resetModules()
|
||||
navigator.navigate.toHome()
|
||||
finish()
|
||||
|
|
|
@ -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,8 +24,9 @@ class SettingsModule(
|
|||
private val messageOptionsStore: MessageOptionsStore,
|
||||
) : ProvidableModule {
|
||||
|
||||
internal fun settingsViewModel(): SettingsViewModel {
|
||||
return SettingsViewModel(
|
||||
internal fun settingsState(): SettingsState {
|
||||
return createStateViewModel {
|
||||
settingsReducer(
|
||||
chatEngine,
|
||||
storeModule.cacheCleaner(),
|
||||
contentResolver,
|
||||
|
@ -33,8 +36,12 @@ class SettingsModule(
|
|||
themeStore,
|
||||
loggingStore,
|
||||
messageOptionsStore,
|
||||
it,
|
||||
JobBag(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal fun eventLogViewModel(): EventLoggerViewModel {
|
||||
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.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<out Page>?) -> 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 {
|
||||
|
|
|
@ -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