mirror of
https://github.com/ouchadam/small-talk.git
synced 2025-02-24 16:07:43 +01:00
Merge pull request #298 from ouchadam/tech/home-reducer
Tech/home reducer
This commit is contained in:
commit
bfc4e83ee7
@ -67,7 +67,7 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ProvidableModule> provide(klass: KClass<T>): T {
|
||||
return when (klass) {
|
||||
DirectoryModule::class -> featureModules.directoryModule
|
||||
|
@ -190,6 +190,9 @@ internal class FeatureModules internal constructor(
|
||||
storeModule.value.applicationStore(),
|
||||
buildMeta,
|
||||
),
|
||||
profileModule,
|
||||
loginModule,
|
||||
directoryModule
|
||||
)
|
||||
}
|
||||
val settingsModule by unsafeLazy {
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit cdf3e1bffba4b69dd8f752c6cc7588b0e89a17af
|
||||
Subproject commit 9017fe3963754199db7c2525ba38a3265ef5701d
|
@ -11,6 +11,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@ -27,6 +28,10 @@ class StartScope(private val scope: CoroutineScope) {
|
||||
fun <T> SharedFlow<T>.launch(onEach: suspend (T) -> Unit) {
|
||||
this.onEach(onEach).launchIn(scope)
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.launch(onEach: suspend (T) -> Unit) {
|
||||
this.onEach(onEach).launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
interface EffectScope {
|
||||
|
@ -16,6 +16,5 @@ class ApplicationPreferences(
|
||||
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class ApplicationVersion(val value: Int)
|
||||
data class ApplicationVersion(val value: Int)
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
package app.dapk.st.directory
|
||||
|
||||
import android.content.Context
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.state.createStateViewModel
|
||||
import app.dapk.st.core.JobBag
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.directory.state.DirectoryEvent
|
||||
import app.dapk.st.directory.state.DirectoryState
|
||||
import app.dapk.st.directory.state.directoryReducer
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.state.createStateViewModel
|
||||
|
||||
class DirectoryModule(
|
||||
private val context: Context,
|
||||
@ -14,6 +15,8 @@ class DirectoryModule(
|
||||
) : ProvidableModule {
|
||||
|
||||
fun directoryState(): DirectoryState {
|
||||
return createStateViewModel { directoryReducer(chatEngine, ShortcutHandler(context), JobBag(), it) }
|
||||
return createStateViewModel { directoryReducer(it) }
|
||||
}
|
||||
|
||||
fun directoryReducer(eventEmitter: suspend (DirectoryEvent) -> Unit) = directoryReducer(chatEngine, ShortcutHandler(context), JobBag(), eventEmitter)
|
||||
}
|
||||
|
@ -8,15 +8,21 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation 'screen-state:screen-android'
|
||||
implementation project(":features:directory")
|
||||
implementation project(":features:login")
|
||||
implementation project(":features:settings")
|
||||
implementation project(":features:profile")
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(':domains:store')
|
||||
implementation 'screen-state:screen-android'
|
||||
implementation project(":core")
|
||||
implementation project(":design-library")
|
||||
implementation libs.compose.coil
|
||||
|
||||
kotlinTest(it)
|
||||
|
||||
testImplementation 'screen-state:state-test'
|
||||
testImplementation 'chat-engine:chat-engine-test'
|
||||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||
}
|
@ -20,7 +20,8 @@ class BetaVersionUpgradeUseCase(
|
||||
}
|
||||
|
||||
private suspend fun hasChangedVersion(): Boolean {
|
||||
val previousVersion = applicationPreferences.readVersion()?.value
|
||||
val readVersion = applicationPreferences.readVersion()
|
||||
val previousVersion = readVersion?.value
|
||||
val currentVersion = buildMeta.versionCode
|
||||
return when (previousVersion) {
|
||||
null -> false
|
||||
|
@ -1,27 +1,51 @@
|
||||
package app.dapk.st.home
|
||||
|
||||
import app.dapk.st.core.JobBag
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.directory.state.DirectoryState
|
||||
import app.dapk.st.directory.DirectoryModule
|
||||
import app.dapk.st.domain.StoreModule
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.login.state.LoginState
|
||||
import app.dapk.st.profile.state.ProfileState
|
||||
import app.dapk.st.home.state.homeReducer
|
||||
import app.dapk.st.login.LoginModule
|
||||
import app.dapk.st.profile.ProfileModule
|
||||
import app.dapk.st.state.State
|
||||
import app.dapk.st.state.createStateViewModel
|
||||
import app.dapk.state.Action
|
||||
import app.dapk.state.DynamicReducers
|
||||
import app.dapk.state.combineReducers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
|
||||
class HomeModule(
|
||||
private val chatEngine: ChatEngine,
|
||||
private val storeModule: StoreModule,
|
||||
val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
||||
private val profileModule: ProfileModule,
|
||||
private val loginModule: LoginModule,
|
||||
private val directoryModule: DirectoryModule,
|
||||
) : ProvidableModule {
|
||||
|
||||
internal fun homeViewModel(directory: DirectoryState, login: LoginState, profile: ProfileState): HomeViewModel {
|
||||
return HomeViewModel(
|
||||
chatEngine,
|
||||
directory,
|
||||
login,
|
||||
profile,
|
||||
storeModule.cacheCleaner(),
|
||||
betaVersionUpgradeUseCase,
|
||||
)
|
||||
internal fun compositeHomeState(): DynamicState {
|
||||
return createStateViewModel {
|
||||
combineReducers(
|
||||
listOf(
|
||||
homeReducerFactory(it),
|
||||
loginModule.loginReducer(it),
|
||||
profileModule.profileReducer(),
|
||||
directoryModule.directoryReducer(it)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
private fun homeReducerFactory(eventEmitter: suspend (Any) -> Unit) =
|
||||
homeReducer(chatEngine, storeModule.cacheCleaner(), betaVersionUpgradeUseCase, JobBag(), eventEmitter)
|
||||
}
|
||||
|
||||
typealias DynamicState = State<DynamicReducers, Any>
|
||||
|
||||
inline fun <reified S, reified E> DynamicState.childState() = object : State<S, E> {
|
||||
override fun dispatch(action: Action) = this@childState.dispatch(action)
|
||||
override val events: Flow<E> = this@childState.events.filterIsInstance()
|
||||
override val current: S = this@childState.current.getState()
|
||||
}
|
||||
|
@ -10,34 +10,39 @@ import app.dapk.st.core.LifecycleEffect
|
||||
import app.dapk.st.core.components.CenteredLoading
|
||||
import app.dapk.st.design.components.CircleishAvatar
|
||||
import app.dapk.st.directory.DirectoryScreen
|
||||
import app.dapk.st.home.HomeScreenState.*
|
||||
import app.dapk.st.home.HomeScreenState.Page.Directory
|
||||
import app.dapk.st.home.HomeScreenState.Page.Profile
|
||||
import app.dapk.st.directory.state.DirectoryState
|
||||
import app.dapk.st.home.state.HomeAction
|
||||
import app.dapk.st.home.state.HomeScreenState.*
|
||||
import app.dapk.st.home.state.HomeScreenState.Page.Directory
|
||||
import app.dapk.st.home.state.HomeScreenState.Page.Profile
|
||||
import app.dapk.st.home.state.HomeState
|
||||
import app.dapk.st.login.LoginScreen
|
||||
import app.dapk.st.login.state.LoginState
|
||||
import app.dapk.st.profile.ProfileScreen
|
||||
import app.dapk.st.profile.state.ProfileState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun HomeScreen(homeViewModel: HomeViewModel) {
|
||||
internal fun HomeScreen(homeState: HomeState, directoryState: DirectoryState, loginState: LoginState, profileState: ProfileState) {
|
||||
LifecycleEffect(
|
||||
onStart = { homeViewModel.start() },
|
||||
onStop = { homeViewModel.stop() }
|
||||
onStart = { homeState.dispatch(HomeAction.LifecycleVisible) },
|
||||
onStop = { homeState.dispatch(HomeAction.LifecycleGone) }
|
||||
)
|
||||
|
||||
when (val state = homeViewModel.state) {
|
||||
when (val state = homeState.current) {
|
||||
Loading -> CenteredLoading()
|
||||
is SignedIn -> {
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
BottomBar(state, homeViewModel)
|
||||
BottomBar(state, homeState)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
Box(modifier = Modifier.padding(innerPadding)) {
|
||||
when (state.page) {
|
||||
Directory -> DirectoryScreen(homeViewModel.directory())
|
||||
Directory -> DirectoryScreen(directoryState)
|
||||
Profile -> {
|
||||
ProfileScreen(homeViewModel.profile()) {
|
||||
homeViewModel.changePage(Directory)
|
||||
ProfileScreen(profileState) {
|
||||
homeState.dispatch(HomeAction.ChangePage(Directory))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -47,8 +52,8 @@ internal fun HomeScreen(homeViewModel: HomeViewModel) {
|
||||
}
|
||||
|
||||
SignedOut -> {
|
||||
LoginScreen(homeViewModel.login()) {
|
||||
homeViewModel.loggedIn()
|
||||
LoginScreen(loginState) {
|
||||
homeState.dispatch(HomeAction.LoggedIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,7 +61,7 @@ internal fun HomeScreen(homeViewModel: HomeViewModel) {
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun BottomBar(state: SignedIn, homeViewModel: HomeViewModel) {
|
||||
private fun BottomBar(state: SignedIn, homeState: HomeState) {
|
||||
Column {
|
||||
Divider(modifier = Modifier.fillMaxWidth(), color = Color.Black.copy(alpha = 0.2f), thickness = 0.5.dp)
|
||||
NavigationBar(containerColor = Color.Transparent, modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||
@ -67,8 +72,8 @@ private fun BottomBar(state: SignedIn, homeViewModel: HomeViewModel) {
|
||||
selected = state.page == page,
|
||||
onClick = {
|
||||
when {
|
||||
state.page == page -> homeViewModel.scrollToTopOfMessages()
|
||||
else -> homeViewModel.changePage(page)
|
||||
state.page == page -> homeState.dispatch(HomeAction.ScrollToTop)
|
||||
else -> homeState.dispatch(HomeAction.ChangePage(page))
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -86,7 +91,7 @@ private fun BottomBar(state: SignedIn, homeViewModel: HomeViewModel) {
|
||||
}
|
||||
},
|
||||
selected = state.page == page,
|
||||
onClick = { homeViewModel.changePage(page) },
|
||||
onClick = { homeState.dispatch(HomeAction.ChangePage(page)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,131 +0,0 @@
|
||||
package app.dapk.st.home
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.dapk.st.directory.state.ComponentLifecycle
|
||||
import app.dapk.st.directory.state.DirectorySideEffect
|
||||
import app.dapk.st.directory.state.DirectoryState
|
||||
import app.dapk.st.domain.StoreCleaner
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.home.HomeScreenState.*
|
||||
import app.dapk.st.login.state.LoginState
|
||||
import app.dapk.st.profile.state.ProfileAction
|
||||
import app.dapk.st.profile.state.ProfileState
|
||||
import app.dapk.st.viewmodel.DapkViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class HomeViewModel(
|
||||
private val chatEngine: ChatEngine,
|
||||
private val directoryState: DirectoryState,
|
||||
private val loginState: LoginState,
|
||||
private val profileState: ProfileState,
|
||||
private val cacheCleaner: StoreCleaner,
|
||||
private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
||||
) : DapkViewModel<HomeScreenState, HomeEvent>(
|
||||
initialState = Loading
|
||||
) {
|
||||
|
||||
private var listenForInvitesJob: Job? = null
|
||||
|
||||
fun directory() = directoryState
|
||||
fun login() = loginState
|
||||
fun profile() = profileState
|
||||
|
||||
fun start() {
|
||||
viewModelScope.launch {
|
||||
state = if (chatEngine.isSignedIn()) {
|
||||
_events.emit(HomeEvent.OnShowContent)
|
||||
initialHomeContent()
|
||||
} else {
|
||||
SignedOut
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
if (chatEngine.isSignedIn()) {
|
||||
listenForInviteChanges()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initialHomeContent(): SignedIn {
|
||||
val me = chatEngine.me(forceRefresh = false)
|
||||
return when (val current = state) {
|
||||
Loading -> SignedIn(Page.Directory, me, invites = 0)
|
||||
is SignedIn -> current.copy(me = me, invites = current.invites)
|
||||
SignedOut -> SignedIn(Page.Directory, me, invites = 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun loggedIn() {
|
||||
viewModelScope.launch {
|
||||
state = initialHomeContent()
|
||||
_events.emit(HomeEvent.OnShowContent)
|
||||
listenForInviteChanges()
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.listenForInviteChanges() {
|
||||
listenForInvitesJob?.cancel()
|
||||
listenForInvitesJob = chatEngine.invites()
|
||||
.onEach { invites ->
|
||||
when (val currentState = state) {
|
||||
is SignedIn -> updateState { currentState.copy(invites = invites.size) }
|
||||
Loading,
|
||||
SignedOut -> {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}.launchIn(this)
|
||||
}
|
||||
|
||||
fun hasVersionChanged() = betaVersionUpgradeUseCase.hasVersionChanged()
|
||||
|
||||
fun clearCache() {
|
||||
viewModelScope.launch {
|
||||
cacheCleaner.cleanCache(removeCredentials = false)
|
||||
betaVersionUpgradeUseCase.notifyUpgraded()
|
||||
_events.emit(HomeEvent.Relaunch)
|
||||
}
|
||||
}
|
||||
|
||||
fun scrollToTopOfMessages() {
|
||||
directoryState.dispatch(DirectorySideEffect.ScrollToTop)
|
||||
}
|
||||
|
||||
fun changePage(page: Page) {
|
||||
state = when (val current = state) {
|
||||
Loading -> current
|
||||
is SignedIn -> {
|
||||
when (page) {
|
||||
current.page -> current
|
||||
else -> current.copy(page = page).also {
|
||||
pageChangeSideEffects(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SignedOut -> current
|
||||
}
|
||||
}
|
||||
|
||||
private fun pageChangeSideEffects(page: Page) {
|
||||
when (page) {
|
||||
Page.Directory -> {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
Page.Profile -> {
|
||||
directoryState.dispatch(ComponentLifecycle.OnGone)
|
||||
profileState.dispatch(ProfileAction.Reset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
// do nothing
|
||||
}
|
||||
}
|
@ -12,26 +12,24 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import app.dapk.st.core.DapkActivity
|
||||
import app.dapk.st.core.extensions.unsafeLazy
|
||||
import app.dapk.st.core.module
|
||||
import app.dapk.st.core.viewModel
|
||||
import app.dapk.st.directory.DirectoryModule
|
||||
import app.dapk.st.login.LoginModule
|
||||
import app.dapk.st.profile.ProfileModule
|
||||
import app.dapk.st.home.state.HomeAction
|
||||
import app.dapk.st.home.state.HomeEvent
|
||||
import app.dapk.st.home.state.HomeState
|
||||
import app.dapk.st.state.state
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class MainActivity : DapkActivity() {
|
||||
|
||||
private val directoryState by state { module<DirectoryModule>().directoryState() }
|
||||
private val loginState by state { module<LoginModule>().loginState() }
|
||||
private val profileState by state { module<ProfileModule>().profileState() }
|
||||
private val homeViewModel by viewModel { module<HomeModule>().homeViewModel(directoryState, loginState, profileState) }
|
||||
private val homeModule by unsafeLazy { module<HomeModule>() }
|
||||
private val compositeState by state { homeModule.compositeHomeState() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pushPermissionLauncher = registerPushPermission()
|
||||
homeViewModel.events.onEach {
|
||||
compositeState.events.onEach {
|
||||
when (it) {
|
||||
HomeEvent.Relaunch -> recreate()
|
||||
HomeEvent.OnShowContent -> pushPermissionLauncher?.invoke()
|
||||
@ -39,11 +37,12 @@ class MainActivity : DapkActivity() {
|
||||
}.launchIn(lifecycleScope)
|
||||
|
||||
setContent {
|
||||
if (homeViewModel.hasVersionChanged()) {
|
||||
BetaUpgradeDialog()
|
||||
val homeState: HomeState = compositeState.childState()
|
||||
if (homeModule.betaVersionUpgradeUseCase.hasVersionChanged()) {
|
||||
BetaUpgradeDialog(homeState)
|
||||
} else {
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
HomeScreen(homeViewModel)
|
||||
HomeScreen(homeState, compositeState.childState(), compositeState.childState(), compositeState.childState())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,20 +55,20 @@ class MainActivity : DapkActivity() {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BetaUpgradeDialog() {
|
||||
AlertDialog(
|
||||
title = { Text(text = "BETA") },
|
||||
text = { Text(text = "During the BETA, version upgrades require a cache clear") },
|
||||
onDismissRequest = {
|
||||
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { homeViewModel.clearCache() }) {
|
||||
Text(text = "Clear cache".uppercase())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BetaUpgradeDialog(homeState: HomeState) {
|
||||
AlertDialog(
|
||||
title = { Text(text = "BETA") },
|
||||
text = { Text(text = "During the BETA, version upgrades require a cache clear") },
|
||||
onDismissRequest = {
|
||||
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { homeState.dispatch(HomeAction.ClearCache) }) {
|
||||
Text(text = "Clear cache".uppercase())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
package app.dapk.st.home.state
|
||||
|
||||
import app.dapk.st.engine.Me
|
||||
import app.dapk.st.home.state.HomeScreenState.Page
|
||||
import app.dapk.state.Action
|
||||
|
||||
sealed interface HomeAction : Action {
|
||||
object LifecycleVisible : HomeAction
|
||||
object LifecycleGone : HomeAction
|
||||
|
||||
object ScrollToTop : HomeAction
|
||||
object ClearCache : HomeAction
|
||||
object LoggedIn : HomeAction
|
||||
|
||||
data class ChangePage(val page: Page) : HomeAction
|
||||
data class ChangePageSideEffect(val page: Page) : HomeAction
|
||||
data class UpdateInvitesCount(val invitesCount: Int) : HomeAction
|
||||
data class UpdateToSignedIn(val me: Me) : HomeAction
|
||||
data class UpdateState(val state: HomeScreenState) : HomeAction
|
||||
object InitialHome : HomeAction
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
package app.dapk.st.home.state
|
||||
|
||||
import app.dapk.st.core.JobBag
|
||||
import app.dapk.st.directory.state.ComponentLifecycle
|
||||
import app.dapk.st.directory.state.DirectorySideEffect
|
||||
import app.dapk.st.domain.StoreCleaner
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.home.BetaVersionUpgradeUseCase
|
||||
import app.dapk.st.profile.state.ProfileAction
|
||||
import app.dapk.state.*
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
fun homeReducer(
|
||||
chatEngine: ChatEngine,
|
||||
cacheCleaner: StoreCleaner,
|
||||
betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
||||
jobBag: JobBag,
|
||||
eventEmitter: suspend (HomeEvent) -> Unit,
|
||||
): ReducerFactory<HomeScreenState> {
|
||||
return createReducer(
|
||||
initialState = HomeScreenState.Loading,
|
||||
|
||||
change(HomeAction.UpdateState::class) { action, _ ->
|
||||
action.state
|
||||
},
|
||||
|
||||
change(HomeAction.UpdateToSignedIn::class) { action, state ->
|
||||
val me = action.me
|
||||
when (state) {
|
||||
HomeScreenState.Loading -> HomeScreenState.SignedIn(HomeScreenState.Page.Directory, me, invites = 0)
|
||||
is HomeScreenState.SignedIn -> state.copy(me = me, invites = state.invites)
|
||||
HomeScreenState.SignedOut -> HomeScreenState.SignedIn(HomeScreenState.Page.Directory, me, invites = 0)
|
||||
}
|
||||
},
|
||||
|
||||
change(HomeAction.UpdateInvitesCount::class) { action, state ->
|
||||
when (state) {
|
||||
HomeScreenState.Loading -> state
|
||||
is HomeScreenState.SignedIn -> state.copy(invites = action.invitesCount)
|
||||
HomeScreenState.SignedOut -> state
|
||||
}
|
||||
},
|
||||
|
||||
async(HomeAction.LifecycleVisible::class) { _ ->
|
||||
if (chatEngine.isSignedIn()) {
|
||||
eventEmitter.invoke(HomeEvent.OnShowContent)
|
||||
dispatch(HomeAction.InitialHome)
|
||||
} else {
|
||||
dispatch(HomeAction.UpdateState(HomeScreenState.SignedOut))
|
||||
}
|
||||
},
|
||||
|
||||
async(HomeAction.InitialHome::class) {
|
||||
val me = chatEngine.me(forceRefresh = false)
|
||||
dispatch(HomeAction.UpdateToSignedIn(me))
|
||||
listenForInviteChanges(chatEngine, jobBag)
|
||||
},
|
||||
|
||||
async(HomeAction.LoggedIn::class) {
|
||||
dispatch(HomeAction.InitialHome)
|
||||
eventEmitter.invoke(HomeEvent.OnShowContent)
|
||||
},
|
||||
|
||||
async(HomeAction.ChangePageSideEffect::class) { action ->
|
||||
when (action.page) {
|
||||
HomeScreenState.Page.Directory -> {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
HomeScreenState.Page.Profile -> {
|
||||
dispatch(ComponentLifecycle.OnGone)
|
||||
dispatch(ProfileAction.Reset)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
multi(HomeAction.ChangePage::class) { action ->
|
||||
change { _, state ->
|
||||
when (state) {
|
||||
is HomeScreenState.SignedIn -> when (action.page) {
|
||||
state.page -> state
|
||||
else -> state.copy(page = action.page)
|
||||
}
|
||||
|
||||
HomeScreenState.Loading -> state
|
||||
HomeScreenState.SignedOut -> state
|
||||
}
|
||||
}
|
||||
async {
|
||||
val state = getState()
|
||||
if (state is HomeScreenState.SignedIn && state.page != action.page) {
|
||||
dispatch(HomeAction.ChangePageSideEffect(action.page))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async(HomeAction.ScrollToTop::class) {
|
||||
dispatch(DirectorySideEffect.ScrollToTop)
|
||||
},
|
||||
|
||||
sideEffect(HomeAction.ClearCache::class) { _, _ ->
|
||||
cacheCleaner.cleanCache(removeCredentials = false)
|
||||
betaVersionUpgradeUseCase.notifyUpgraded()
|
||||
eventEmitter.invoke(HomeEvent.Relaunch)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun ReducerScope<HomeScreenState>.listenForInviteChanges(chatEngine: ChatEngine, jobBag: JobBag) {
|
||||
jobBag.replace(
|
||||
"invites-count",
|
||||
chatEngine.invites()
|
||||
.onEach { invites ->
|
||||
when (getState()) {
|
||||
is HomeScreenState.SignedIn -> dispatch(HomeAction.UpdateInvitesCount(invites.size))
|
||||
HomeScreenState.Loading,
|
||||
HomeScreenState.SignedOut -> {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}.launchIn(coroutineScope)
|
||||
)
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
package app.dapk.st.home
|
||||
package app.dapk.st.home.state
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import app.dapk.st.engine.Me
|
||||
import app.dapk.st.state.State
|
||||
|
||||
typealias HomeState = State<HomeScreenState, HomeEvent>
|
||||
|
||||
sealed interface HomeScreenState {
|
||||
|
@ -0,0 +1,68 @@
|
||||
package app.dapk.st.home
|
||||
|
||||
import app.dapk.st.core.BuildMeta
|
||||
import app.dapk.st.domain.ApplicationPreferences
|
||||
import app.dapk.st.domain.ApplicationVersion
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
import test.delegateReturn
|
||||
import test.expect
|
||||
|
||||
class BetaVersionUpgradeUseCaseTest {
|
||||
|
||||
private val buildMeta = BuildMeta(versionName = "a-version-name", versionCode = 100, isDebug = false)
|
||||
private val fakeApplicationPreferences = FakeApplicationPreferences()
|
||||
|
||||
private val useCase = BetaVersionUpgradeUseCase(
|
||||
fakeApplicationPreferences.instance,
|
||||
buildMeta
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `given same stored version, when hasVersionChanged then is false`() = runTest {
|
||||
fakeApplicationPreferences.givenVersion().returns(ApplicationVersion(buildMeta.versionCode))
|
||||
|
||||
val result = useCase.hasVersionChanged()
|
||||
|
||||
result shouldBeEqualTo false
|
||||
}
|
||||
|
||||
// Should be impossible
|
||||
@Test
|
||||
fun `given higher stored version, when hasVersionChanged then is false`() = runTest {
|
||||
fakeApplicationPreferences.givenVersion().returns(ApplicationVersion(buildMeta.versionCode + 1))
|
||||
|
||||
val result = useCase.hasVersionChanged()
|
||||
|
||||
result shouldBeEqualTo false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given lower stored version, when hasVersionChanged then is true`() = runTest {
|
||||
fakeApplicationPreferences.givenVersion().returns(ApplicationVersion(buildMeta.versionCode - 1))
|
||||
|
||||
val result = useCase.hasVersionChanged()
|
||||
|
||||
result shouldBeEqualTo true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given version has changed, when waiting, then blocks until notified of upgrade`() = runTest {
|
||||
fakeApplicationPreferences.givenVersion().returns(ApplicationVersion(buildMeta.versionCode - 1))
|
||||
fakeApplicationPreferences.instance.expect { it.setVersion(ApplicationVersion(buildMeta.versionCode)) }
|
||||
|
||||
val waitUntilReady = async { useCase.waitUnitReady() }
|
||||
async { useCase.notifyUpgraded() }
|
||||
waitUntilReady.await()
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeApplicationPreferences {
|
||||
val instance = mockk<ApplicationPreferences>()
|
||||
|
||||
fun givenVersion() = coEvery { instance.readVersion() }.delegateReturn()
|
||||
}
|
@ -0,0 +1,224 @@
|
||||
package app.dapk.st.home.state
|
||||
|
||||
import app.dapk.st.directory.state.ComponentLifecycle
|
||||
import app.dapk.st.directory.state.DirectorySideEffect
|
||||
import app.dapk.st.domain.StoreCleaner
|
||||
import app.dapk.st.engine.Me
|
||||
import app.dapk.st.home.BetaVersionUpgradeUseCase
|
||||
import app.dapk.st.matrix.common.HomeServerUrl
|
||||
import app.dapk.st.profile.state.ProfileAction
|
||||
import fake.FakeChatEngine
|
||||
import fake.FakeJobBag
|
||||
import fixture.aRoomId
|
||||
import fixture.aRoomInvite
|
||||
import fixture.aUserId
|
||||
import io.mockk.mockk
|
||||
import org.junit.Test
|
||||
import test.*
|
||||
|
||||
private val A_ME = Me(aUserId(), displayName = null, avatarUrl = null, homeServerUrl = HomeServerUrl("ignored"))
|
||||
private val A_SIGNED_IN_STATE = HomeScreenState.SignedIn(
|
||||
HomeScreenState.Page.Directory,
|
||||
me = A_ME,
|
||||
invites = 0,
|
||||
)
|
||||
|
||||
class HomeReducerTest {
|
||||
|
||||
private val fakeStoreCleaner = FakeStoreCleaner()
|
||||
private val fakeChatEngine = FakeChatEngine()
|
||||
private val fakeBetaVersionUpgradeUseCase = FakeBetaVersionUpgradeUseCase()
|
||||
private val fakeJobBag = FakeJobBag()
|
||||
|
||||
private val runReducerTest = testReducer { fakeEventSource ->
|
||||
homeReducer(
|
||||
fakeChatEngine,
|
||||
fakeStoreCleaner,
|
||||
fakeBetaVersionUpgradeUseCase.instance,
|
||||
fakeJobBag.instance,
|
||||
fakeEventSource,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state is loading`() = runReducerTest {
|
||||
assertInitialState(HomeScreenState.Loading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when UpdateState, then replaces state`() = runReducerTest {
|
||||
reduce(HomeAction.UpdateState(HomeScreenState.SignedOut))
|
||||
|
||||
assertOnlyStateChange(HomeScreenState.SignedOut)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given SignedIn, when UpdateInviteCount, then updates invite count`() = runReducerTest {
|
||||
setState(A_SIGNED_IN_STATE)
|
||||
|
||||
reduce(HomeAction.UpdateInvitesCount(invitesCount = 90))
|
||||
|
||||
assertOnlyStateChange(A_SIGNED_IN_STATE.copy(invites = 90))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when ScrollToTop, then forwards to directory scroll event`() = runReducerTest {
|
||||
reduce(HomeAction.ScrollToTop)
|
||||
|
||||
assertOnlyDispatches(DirectorySideEffect.ScrollToTop)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when ClearCache, then clears store cache, upgrades and relaunches`() = runReducerTest {
|
||||
fakeStoreCleaner.expect { it.cleanCache(removeCredentials = false) }
|
||||
fakeBetaVersionUpgradeUseCase.instance.expect { it.notifyUpgraded() }
|
||||
|
||||
reduce(HomeAction.ClearCache)
|
||||
|
||||
assertOnlyEvents(HomeEvent.Relaunch)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given SignedIn and invites update, when Visible, then show content and update on invite changes`() = runReducerTest {
|
||||
fakeChatEngine.givenIsSignedIn().returns(true)
|
||||
|
||||
reduce(HomeAction.LifecycleVisible)
|
||||
|
||||
assertEvents(HomeEvent.OnShowContent)
|
||||
assertDispatches(HomeAction.InitialHome)
|
||||
assertNoStateChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given SignedOut and invites update, when Visible, then show content and update on invite changes`() = runReducerTest {
|
||||
fakeChatEngine.givenIsSignedIn().returns(false)
|
||||
|
||||
reduce(HomeAction.LifecycleVisible)
|
||||
|
||||
assertOnlyDispatches(HomeAction.UpdateState(HomeScreenState.SignedOut))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given SignedIn, when InitialHome, then updates me state and listens to invite changes`() = runReducerTest {
|
||||
setState(A_SIGNED_IN_STATE)
|
||||
fakeChatEngine.givenMe(forceRefresh = false).returns(A_ME)
|
||||
givenInvites(count = 5)
|
||||
|
||||
reduce(HomeAction.InitialHome)
|
||||
|
||||
assertOnlyDispatches(
|
||||
HomeAction.UpdateToSignedIn(A_ME),
|
||||
HomeAction.UpdateInvitesCount(5)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given SignedIn, when UpdateToSignedIn, then updates me state`() = runReducerTest {
|
||||
setState(A_SIGNED_IN_STATE)
|
||||
val expectedMe = A_ME.copy(aUserId("another-user"))
|
||||
|
||||
reduce(HomeAction.UpdateToSignedIn(expectedMe))
|
||||
|
||||
assertOnlyStateChange(A_SIGNED_IN_STATE.copy(me = expectedMe))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given Loading, when UpdateToSignedIn, then set SignedIn and updates me state`() = runReducerTest {
|
||||
setState(HomeScreenState.Loading)
|
||||
val expectedMe = A_ME.copy(aUserId("another-user"))
|
||||
|
||||
reduce(HomeAction.UpdateToSignedIn(expectedMe))
|
||||
|
||||
assertOnlyStateChange(A_SIGNED_IN_STATE.copy(me = expectedMe))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given SignedOut, when UpdateToSignedIn, then set SignedIn and updates me state`() = runReducerTest {
|
||||
setState(HomeScreenState.SignedOut)
|
||||
val expectedMe = A_ME.copy(aUserId("another-user"))
|
||||
|
||||
reduce(HomeAction.UpdateToSignedIn(expectedMe))
|
||||
|
||||
assertOnlyStateChange(A_SIGNED_IN_STATE.copy(me = expectedMe))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when LoggedIn, then emit show content and fetch initial home`() = runReducerTest {
|
||||
setState(HomeScreenState.SignedOut)
|
||||
givenInvites(count = 0)
|
||||
|
||||
reduce(HomeAction.LoggedIn)
|
||||
|
||||
assertDispatches(HomeAction.InitialHome)
|
||||
assertEvents(HomeEvent.OnShowContent)
|
||||
assertNoStateChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given SignedOut, when ChangePage, then does nothing`() = runReducerTest {
|
||||
setState(HomeScreenState.SignedOut)
|
||||
|
||||
reduce(HomeAction.ChangePage(HomeScreenState.Page.Directory))
|
||||
|
||||
assertNoChanges()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given Loading, when ChangePage, then does nothing`() = runReducerTest {
|
||||
setState(HomeScreenState.Loading)
|
||||
|
||||
reduce(HomeAction.ChangePage(HomeScreenState.Page.Directory))
|
||||
|
||||
assertNoChanges()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given SignedIn, when ChangePage to same page, then does nothing`() = runReducerTest {
|
||||
val page = HomeScreenState.Page.Directory
|
||||
setState(A_SIGNED_IN_STATE.copy(page = page))
|
||||
|
||||
reduce(HomeAction.ChangePage(page))
|
||||
|
||||
assertNoChanges()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given SignedIn, when ChangePage to different page, then updates page and emits side effect`() = runReducerTest {
|
||||
val expectedPage = HomeScreenState.Page.Profile
|
||||
setState(A_SIGNED_IN_STATE.copy(page = HomeScreenState.Page.Directory))
|
||||
|
||||
reduce(HomeAction.ChangePage(expectedPage))
|
||||
|
||||
assertStateChange(A_SIGNED_IN_STATE.copy(page = expectedPage))
|
||||
assertDispatches(HomeAction.ChangePageSideEffect(expectedPage))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when ChangePageSide is Directory, then does nothing`() = runReducerTest {
|
||||
reduce(HomeAction.ChangePageSideEffect(HomeScreenState.Page.Directory))
|
||||
|
||||
assertNoChanges()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when ChangePageSide is Profile, then mark directory gone and resets profile`() = runReducerTest {
|
||||
reduce(HomeAction.ChangePageSideEffect(HomeScreenState.Page.Profile))
|
||||
|
||||
assertOnlyDispatches(
|
||||
ComponentLifecycle.OnGone,
|
||||
ProfileAction.Reset
|
||||
)
|
||||
}
|
||||
|
||||
private fun givenInvites(count: Int) {
|
||||
fakeJobBag.instance.expect { it.replace("invites-count", any()) }
|
||||
val invites = List(count) { aRoomInvite(roomId = aRoomId(it.toString())) }
|
||||
fakeChatEngine.givenInvites().emits(invites)
|
||||
}
|
||||
}
|
||||
|
||||
class FakeStoreCleaner : StoreCleaner by mockk()
|
||||
|
||||
class FakeBetaVersionUpgradeUseCase {
|
||||
val instance = mockk<BetaVersionUpgradeUseCase>()
|
||||
}
|
@ -3,11 +3,10 @@ package app.dapk.st.login
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.login.state.LoginState
|
||||
import app.dapk.st.login.state.LoginUseCase
|
||||
import app.dapk.st.login.state.loginReducer
|
||||
import app.dapk.st.login.state.*
|
||||
import app.dapk.st.push.PushModule
|
||||
import app.dapk.st.state.createStateViewModel
|
||||
import app.dapk.state.ReducerFactory
|
||||
|
||||
class LoginModule(
|
||||
private val chatEngine: ChatEngine,
|
||||
@ -17,8 +16,12 @@ class LoginModule(
|
||||
|
||||
fun loginState(): LoginState {
|
||||
return createStateViewModel {
|
||||
val loginUseCase = LoginUseCase(chatEngine, pushModule.pushTokenRegistrars(), errorTracker)
|
||||
loginReducer(loginUseCase, it)
|
||||
loginReducer(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun loginReducer(eventEmitter: suspend (LoginEvent) -> Unit): ReducerFactory<LoginScreenState> {
|
||||
val loginUseCase = LoginUseCase(chatEngine, pushModule.pushTokenRegistrars(), errorTracker)
|
||||
return loginReducer(loginUseCase, eventEmitter)
|
||||
}
|
||||
}
|
@ -15,7 +15,9 @@ class ProfileModule(
|
||||
) : ProvidableModule {
|
||||
|
||||
fun profileState(): ProfileState {
|
||||
return createStateViewModel { profileReducer(chatEngine, errorTracker, ProfileUseCase(chatEngine, errorTracker), JobBag()) }
|
||||
return createStateViewModel { profileReducer() }
|
||||
}
|
||||
|
||||
fun profileReducer() = profileReducer(chatEngine, errorTracker, ProfileUseCase(chatEngine, errorTracker), JobBag())
|
||||
|
||||
}
|
@ -144,7 +144,6 @@ internal fun settingsReducer(
|
||||
}
|
||||
|
||||
Ignored -> {
|
||||
nothing()
|
||||
}
|
||||
|
||||
ToggleDynamicTheme -> async {
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit ea31ab26de443ed5e6bb67ce594e3ce8d5f04ff3
|
||||
Subproject commit d596949ac2b923b02da55ddd78e2e26dc46af82a
|
Loading…
x
Reference in New Issue
Block a user