Merge pull request #260 from ouchadam/tech/profile-reducer
Porting ProfileViewModel to reducer
This commit is contained in:
commit
8885406a74
|
@ -12,13 +12,10 @@ import java.io.InputStream
|
||||||
class FakeChatEngine : ChatEngine by mockk() {
|
class FakeChatEngine : ChatEngine by mockk() {
|
||||||
|
|
||||||
fun givenMessages(roomId: RoomId, disableReadReceipts: Boolean) = every { messages(roomId, disableReadReceipts) }.delegateReturn()
|
fun givenMessages(roomId: RoomId, disableReadReceipts: Boolean) = every { messages(roomId, disableReadReceipts) }.delegateReturn()
|
||||||
|
|
||||||
fun givenDirectory() = every { directory() }.delegateReturn()
|
fun givenDirectory() = every { directory() }.delegateReturn()
|
||||||
|
|
||||||
fun givenImportKeys(inputStream: InputStream, passphrase: String) = coEvery { inputStream.importRoomKeys(passphrase) }.delegateReturn()
|
fun givenImportKeys(inputStream: InputStream, passphrase: String) = coEvery { inputStream.importRoomKeys(passphrase) }.delegateReturn()
|
||||||
|
|
||||||
fun givenNotificationsInvites() = every { notificationsInvites() }.delegateEmit()
|
fun givenNotificationsInvites() = every { notificationsInvites() }.delegateEmit()
|
||||||
|
|
||||||
fun givenNotificationsMessages() = every { notificationsMessages() }.delegateEmit()
|
fun givenNotificationsMessages() = every { notificationsMessages() }.delegateEmit()
|
||||||
|
fun givenInvites() = every { invites() }.delegateEmit()
|
||||||
|
fun givenMe(forceRefresh: Boolean) = coEvery { me(forceRefresh) }.delegateReturn()
|
||||||
}
|
}
|
|
@ -25,4 +25,8 @@ class JobBag {
|
||||||
jobs.remove(key.java.canonicalName)?.cancel()
|
jobs.remove(key.java.canonicalName)?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cancelAll() {
|
||||||
|
jobs.values.forEach { it.cancel() }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -3,7 +3,6 @@ applyAndroidComposeLibraryModule(project)
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":chat-engine")
|
implementation project(":chat-engine")
|
||||||
implementation project(":domains:android:compose-core")
|
implementation project(":domains:android:compose-core")
|
||||||
implementation project(":domains:android:viewmodel")
|
|
||||||
implementation project(":domains:state")
|
implementation project(":domains:state")
|
||||||
implementation project(":features:messenger")
|
implementation project(":features:messenger")
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
|
@ -16,7 +15,6 @@ dependencies {
|
||||||
androidImportFixturesWorkaround(project, project(":core"))
|
androidImportFixturesWorkaround(project, project(":core"))
|
||||||
androidImportFixturesWorkaround(project, project(":domains:state"))
|
androidImportFixturesWorkaround(project, project(":domains:state"))
|
||||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||||
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
|
||||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||||
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||||
}
|
}
|
|
@ -5,7 +5,7 @@ import app.dapk.st.directory.state.DirectoryState
|
||||||
import app.dapk.st.domain.StoreModule
|
import app.dapk.st.domain.StoreModule
|
||||||
import app.dapk.st.engine.ChatEngine
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.login.LoginViewModel
|
import app.dapk.st.login.LoginViewModel
|
||||||
import app.dapk.st.profile.ProfileViewModel
|
import app.dapk.st.profile.state.ProfileState
|
||||||
|
|
||||||
class HomeModule(
|
class HomeModule(
|
||||||
private val chatEngine: ChatEngine,
|
private val chatEngine: ChatEngine,
|
||||||
|
@ -13,13 +13,13 @@ class HomeModule(
|
||||||
val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
internal fun homeViewModel(directory: DirectoryState, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel {
|
internal fun homeViewModel(directory: DirectoryState, login: LoginViewModel, profile: ProfileState): HomeViewModel {
|
||||||
return HomeViewModel(
|
return HomeViewModel(
|
||||||
chatEngine,
|
chatEngine,
|
||||||
storeModule.credentialsStore(),
|
storeModule.credentialsStore(),
|
||||||
directory,
|
directory,
|
||||||
login,
|
login,
|
||||||
profileViewModel,
|
profile,
|
||||||
storeModule.cacheCleaner(),
|
storeModule.cacheCleaner(),
|
||||||
betaVersionUpgradeUseCase,
|
betaVersionUpgradeUseCase,
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,7 +10,8 @@ import app.dapk.st.home.HomeScreenState.*
|
||||||
import app.dapk.st.login.LoginViewModel
|
import app.dapk.st.login.LoginViewModel
|
||||||
import app.dapk.st.matrix.common.CredentialsStore
|
import app.dapk.st.matrix.common.CredentialsStore
|
||||||
import app.dapk.st.matrix.common.isSignedIn
|
import app.dapk.st.matrix.common.isSignedIn
|
||||||
import app.dapk.st.profile.ProfileViewModel
|
import app.dapk.st.profile.state.ProfileAction
|
||||||
|
import app.dapk.st.profile.state.ProfileState
|
||||||
import app.dapk.st.viewmodel.DapkViewModel
|
import app.dapk.st.viewmodel.DapkViewModel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -24,7 +25,7 @@ internal class HomeViewModel(
|
||||||
private val credentialsProvider: CredentialsStore,
|
private val credentialsProvider: CredentialsStore,
|
||||||
private val directoryState: DirectoryState,
|
private val directoryState: DirectoryState,
|
||||||
private val loginViewModel: LoginViewModel,
|
private val loginViewModel: LoginViewModel,
|
||||||
private val profileViewModel: ProfileViewModel,
|
private val profileState: ProfileState,
|
||||||
private val cacheCleaner: StoreCleaner,
|
private val cacheCleaner: StoreCleaner,
|
||||||
private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
||||||
) : DapkViewModel<HomeScreenState, HomeEvent>(
|
) : DapkViewModel<HomeScreenState, HomeEvent>(
|
||||||
|
@ -35,7 +36,7 @@ internal class HomeViewModel(
|
||||||
|
|
||||||
fun directory() = directoryState
|
fun directory() = directoryState
|
||||||
fun login() = loginViewModel
|
fun login() = loginViewModel
|
||||||
fun profile() = profileViewModel
|
fun profile() = profileState
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
@ -125,7 +126,7 @@ internal class HomeViewModel(
|
||||||
|
|
||||||
Page.Profile -> {
|
Page.Profile -> {
|
||||||
directoryState.dispatch(ComponentLifecycle.OnGone)
|
directoryState.dispatch(ComponentLifecycle.OnGone)
|
||||||
profileViewModel.reset()
|
profileState.dispatch(ProfileAction.Reset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ class MainActivity : DapkActivity() {
|
||||||
|
|
||||||
private val directoryViewModel by state { module<DirectoryModule>().directoryState() }
|
private val directoryViewModel by state { module<DirectoryModule>().directoryState() }
|
||||||
private val loginViewModel by viewModel { module<LoginModule>().loginViewModel() }
|
private val loginViewModel by viewModel { module<LoginModule>().loginViewModel() }
|
||||||
private val profileViewModel by viewModel { module<ProfileModule>().profileViewModel() }
|
private val profileViewModel by state { module<ProfileModule>().profileState() }
|
||||||
private val homeViewModel by viewModel { module<HomeModule>().homeViewModel(directoryViewModel, loginViewModel, profileViewModel) }
|
private val homeViewModel by viewModel { module<HomeModule>().homeViewModel(directoryViewModel, loginViewModel, profileViewModel) }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
|
@ -4,8 +4,17 @@ dependencies {
|
||||||
implementation project(":chat-engine")
|
implementation project(":chat-engine")
|
||||||
implementation project(":features:settings")
|
implementation project(":features:settings")
|
||||||
implementation project(':domains:store')
|
implementation project(':domains:store')
|
||||||
|
implementation project(':domains:state')
|
||||||
implementation project(":domains:android:compose-core")
|
implementation project(":domains:android:compose-core")
|
||||||
implementation project(":domains:android:viewmodel")
|
|
||||||
implementation project(":design-library")
|
implementation project(":design-library")
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
|
|
||||||
|
kotlinTest(it)
|
||||||
|
|
||||||
|
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":core"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":domains:state"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||||
}
|
}
|
|
@ -1,16 +1,21 @@
|
||||||
package app.dapk.st.profile
|
package app.dapk.st.profile
|
||||||
|
|
||||||
|
import app.dapk.st.core.JobBag
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
|
import app.dapk.st.core.createStateViewModel
|
||||||
import app.dapk.st.core.extensions.ErrorTracker
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
import app.dapk.st.engine.ChatEngine
|
import app.dapk.st.engine.ChatEngine
|
||||||
|
import app.dapk.st.profile.state.ProfileState
|
||||||
|
import app.dapk.st.profile.state.ProfileUseCase
|
||||||
|
import app.dapk.st.profile.state.profileReducer
|
||||||
|
|
||||||
class ProfileModule(
|
class ProfileModule(
|
||||||
private val chatEngine: ChatEngine,
|
private val chatEngine: ChatEngine,
|
||||||
private val errorTracker: ErrorTracker,
|
private val errorTracker: ErrorTracker,
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
fun profileViewModel(): ProfileViewModel {
|
fun profileState(): ProfileState {
|
||||||
return ProfileViewModel(chatEngine, errorTracker)
|
return createStateViewModel { profileReducer(chatEngine, errorTracker, ProfileUseCase(chatEngine, errorTracker), JobBag()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -20,18 +20,22 @@ import androidx.compose.ui.unit.dp
|
||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
import app.dapk.st.core.LifecycleEffect
|
import app.dapk.st.core.LifecycleEffect
|
||||||
import app.dapk.st.core.components.CenteredLoading
|
import app.dapk.st.core.components.CenteredLoading
|
||||||
|
import app.dapk.st.core.page.PageAction
|
||||||
import app.dapk.st.design.components.*
|
import app.dapk.st.design.components.*
|
||||||
import app.dapk.st.engine.RoomInvite
|
import app.dapk.st.engine.RoomInvite
|
||||||
import app.dapk.st.engine.RoomInvite.InviteMeta
|
import app.dapk.st.engine.RoomInvite.InviteMeta
|
||||||
|
import app.dapk.st.profile.state.Page
|
||||||
|
import app.dapk.st.profile.state.ProfileAction
|
||||||
|
import app.dapk.st.profile.state.ProfileState
|
||||||
import app.dapk.st.settings.SettingsActivity
|
import app.dapk.st.settings.SettingsActivity
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(viewModel: ProfileViewModel, onTopLevelBack: () -> Unit) {
|
fun ProfileScreen(viewModel: ProfileState, onTopLevelBack: () -> Unit) {
|
||||||
viewModel.ObserveEvents()
|
viewModel.ObserveEvents()
|
||||||
|
|
||||||
LifecycleEffect(
|
LifecycleEffect(
|
||||||
onStart = { viewModel.start() },
|
onStart = { viewModel.dispatch(ProfileAction.ComponentLifecycle.Visible) },
|
||||||
onStop = { viewModel.stop() }
|
onStop = { viewModel.dispatch(ProfileAction.ComponentLifecycle.Gone) }
|
||||||
)
|
)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
@ -39,11 +43,11 @@ fun ProfileScreen(viewModel: ProfileViewModel, onTopLevelBack: () -> Unit) {
|
||||||
val onNavigate: (SpiderPage<out Page>?) -> Unit = {
|
val onNavigate: (SpiderPage<out Page>?) -> Unit = {
|
||||||
when (it) {
|
when (it) {
|
||||||
null -> onTopLevelBack()
|
null -> onTopLevelBack()
|
||||||
else -> viewModel.goTo(it)
|
else -> viewModel.dispatch(PageAction.GoTo(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) {
|
Spider(currentPage = viewModel.current.state1.page, onNavigate = onNavigate) {
|
||||||
item(Page.Routes.profile) {
|
item(Page.Routes.profile) {
|
||||||
ProfilePage(context, viewModel, it)
|
ProfilePage(context, viewModel, it)
|
||||||
}
|
}
|
||||||
|
@ -54,7 +58,7 @@ fun ProfileScreen(viewModel: ProfileViewModel, onTopLevelBack: () -> Unit) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile: Page.Profile) {
|
private fun ProfilePage(context: Context, viewModel: ProfileState, profile: Page.Profile) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
@ -67,7 +71,7 @@ private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile:
|
||||||
|
|
||||||
when (val state = profile.content) {
|
when (val state = profile.content) {
|
||||||
is Lce.Loading -> CenteredLoading()
|
is Lce.Loading -> CenteredLoading()
|
||||||
is Lce.Error -> GenericError { viewModel.start() }
|
is Lce.Error -> GenericError { viewModel.dispatch(ProfileAction.ComponentLifecycle.Visible) }
|
||||||
is Lce.Content -> {
|
is Lce.Content -> {
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val content = state.value
|
val content = state.value
|
||||||
|
@ -111,7 +115,7 @@ private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile:
|
||||||
TextRow(
|
TextRow(
|
||||||
title = "Invitations",
|
title = "Invitations",
|
||||||
content = "${content.invitationsCount} pending",
|
content = "${content.invitationsCount} pending",
|
||||||
onClick = { viewModel.goToInvitations() }
|
onClick = { viewModel.dispatch(ProfileAction.GoToInvitations) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,7 +123,7 @@ private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile:
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SpiderItemScope.Invitations(viewModel: ProfileViewModel, invitations: Page.Invitations) {
|
private fun SpiderItemScope.Invitations(viewModel: ProfileState, invitations: Page.Invitations) {
|
||||||
when (val state = invitations.content) {
|
when (val state = invitations.content) {
|
||||||
is Lce.Loading -> CenteredLoading()
|
is Lce.Loading -> CenteredLoading()
|
||||||
is Lce.Content -> {
|
is Lce.Content -> {
|
||||||
|
@ -133,11 +137,11 @@ private fun SpiderItemScope.Invitations(viewModel: ProfileViewModel, invitations
|
||||||
TextRow(title = text, includeDivider = false) {
|
TextRow(title = text, includeDivider = false) {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Row {
|
Row {
|
||||||
Button(modifier = Modifier.weight(1f), onClick = { viewModel.rejectRoomInvite(it.roomId) }) {
|
Button(modifier = Modifier.weight(1f), onClick = { viewModel.dispatch(ProfileAction.RejectRoomInvite(it.roomId)) }) {
|
||||||
Text("Reject".uppercase())
|
Text("Reject".uppercase())
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.fillMaxWidth(0.1f))
|
Spacer(modifier = Modifier.fillMaxWidth(0.1f))
|
||||||
Button(modifier = Modifier.weight(1f), onClick = { viewModel.acceptRoomInvite(it.roomId) }) {
|
Button(modifier = Modifier.weight(1f), onClick = { viewModel.dispatch(ProfileAction.AcceptRoomInvite(it.roomId)) }) {
|
||||||
Text("Accept".uppercase())
|
Text("Accept".uppercase())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +158,7 @@ private fun SpiderItemScope.Invitations(viewModel: ProfileViewModel, invitations
|
||||||
private fun RoomInvite.inviterName() = this.from.displayName?.let { "$it (${this.from.id.value})" } ?: this.from.id.value
|
private fun RoomInvite.inviterName() = this.from.displayName?.let { "$it (${this.from.id.value})" } ?: this.from.id.value
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProfileViewModel.ObserveEvents() {
|
private fun ProfileState.ObserveEvents() {
|
||||||
// StartObserving {
|
// StartObserving {
|
||||||
// this@ObserveEvents.events.launch {
|
// this@ObserveEvents.events.launch {
|
||||||
// when (it) {
|
// when (it) {
|
||||||
|
|
|
@ -1,142 +0,0 @@
|
||||||
package app.dapk.st.profile
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import app.dapk.st.core.Lce
|
|
||||||
import app.dapk.st.core.extensions.ErrorTracker
|
|
||||||
import app.dapk.st.design.components.SpiderPage
|
|
||||||
import app.dapk.st.engine.ChatEngine
|
|
||||||
import app.dapk.st.matrix.common.RoomId
|
|
||||||
import app.dapk.st.viewmodel.DapkViewModel
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class ProfileViewModel(
|
|
||||||
private val chatEngine: ChatEngine,
|
|
||||||
private val errorTracker: ErrorTracker,
|
|
||||||
) : DapkViewModel<ProfileScreenState, ProfileEvent>(
|
|
||||||
ProfileScreenState(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false))
|
|
||||||
) {
|
|
||||||
|
|
||||||
private var currentPageJob: Job? = null
|
|
||||||
|
|
||||||
fun start() {
|
|
||||||
goToProfile()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun goToProfile() {
|
|
||||||
combine(
|
|
||||||
flow {
|
|
||||||
val result = runCatching { chatEngine.me(forceRefresh = true) }
|
|
||||||
.onFailure { errorTracker.track(it, "Loading profile") }
|
|
||||||
emit(result)
|
|
||||||
},
|
|
||||||
chatEngine.invites(),
|
|
||||||
transform = { me, invites -> me to invites }
|
|
||||||
)
|
|
||||||
.onEach { (me, invites) ->
|
|
||||||
updatePageState<Page.Profile> {
|
|
||||||
when (me.isSuccess) {
|
|
||||||
true -> copy(content = Lce.Content(Page.Profile.Content(me.getOrThrow(), invites.size)))
|
|
||||||
false -> copy(content = Lce.Error(me.exceptionOrNull()!!))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.launchPageJob()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun goToInvitations() {
|
|
||||||
updateState { copy(page = SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading()))) }
|
|
||||||
|
|
||||||
chatEngine.invites()
|
|
||||||
.onEach {
|
|
||||||
updatePageState<Page.Invitations> {
|
|
||||||
copy(content = Lce.Content(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.launchPageJob()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun goTo(page: SpiderPage<out Page>) {
|
|
||||||
currentPageJob?.cancel()
|
|
||||||
updateState { copy(page = page) }
|
|
||||||
when (page.state) {
|
|
||||||
is Page.Invitations -> goToInvitations()
|
|
||||||
is Page.Profile -> goToProfile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> Flow<T>.launchPageJob() {
|
|
||||||
currentPageJob?.cancel()
|
|
||||||
currentPageJob = this.launchIn(viewModelScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateDisplayName() {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateAvatar() {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
fun acceptRoomInvite(roomId: RoomId) {
|
|
||||||
launchCatching { chatEngine.joinRoom(roomId) }.fold(
|
|
||||||
onError = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun rejectRoomInvite(roomId: RoomId) {
|
|
||||||
launchCatching { chatEngine.rejectJoinRoom(roomId) }.fold(
|
|
||||||
onError = {
|
|
||||||
Log.e("!!!", it.message, it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@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))) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
when (state.page.state) {
|
|
||||||
is Page.Invitations -> updateState {
|
|
||||||
ProfileScreenState(
|
|
||||||
SpiderPage(
|
|
||||||
Page.Routes.profile,
|
|
||||||
"Profile",
|
|
||||||
null,
|
|
||||||
Page.Profile(Lce.Loading()),
|
|
||||||
hasToolbar = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is Page.Profile -> {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
currentPageJob?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <S, VE, T> DapkViewModel<S, VE>.launchCatching(block: suspend () -> T): LaunchCatching<T> {
|
|
||||||
return object : LaunchCatching<T> {
|
|
||||||
override fun fold(onSuccess: (T) -> Unit, onError: (Throwable) -> Unit) {
|
|
||||||
viewModelScope.launch { runCatching { block() }.fold(onSuccess, onError) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LaunchCatching<T> {
|
|
||||||
fun fold(onSuccess: (T) -> Unit = {}, onError: (Throwable) -> Unit = {})
|
|
||||||
}
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package app.dapk.st.profile.state
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.common.RoomId
|
||||||
|
import app.dapk.state.Action
|
||||||
|
|
||||||
|
sealed interface ProfileAction : Action {
|
||||||
|
|
||||||
|
sealed interface ComponentLifecycle : ProfileAction {
|
||||||
|
object Visible : ComponentLifecycle
|
||||||
|
object Gone : ComponentLifecycle
|
||||||
|
}
|
||||||
|
|
||||||
|
object GoToInvitations : ProfileAction
|
||||||
|
data class AcceptRoomInvite(val roomId: RoomId) : ProfileAction
|
||||||
|
data class RejectRoomInvite(val roomId: RoomId) : ProfileAction
|
||||||
|
object Reset : ProfileAction
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package app.dapk.st.profile.state
|
||||||
|
|
||||||
|
import app.dapk.st.core.JobBag
|
||||||
|
import app.dapk.st.core.Lce
|
||||||
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
|
import app.dapk.st.core.page.PageAction
|
||||||
|
import app.dapk.st.core.page.PageStateChange
|
||||||
|
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.engine.ChatEngine
|
||||||
|
import app.dapk.state.async
|
||||||
|
import app.dapk.state.createReducer
|
||||||
|
import app.dapk.state.sideEffect
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
|
fun profileReducer(
|
||||||
|
chatEngine: ChatEngine,
|
||||||
|
errorTracker: ErrorTracker,
|
||||||
|
profileUseCase: ProfileUseCase,
|
||||||
|
jobBag: JobBag,
|
||||||
|
) = createPageReducer(
|
||||||
|
initialPage = SpiderPage<Page>(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false),
|
||||||
|
factory = {
|
||||||
|
createReducer(
|
||||||
|
initialState = Unit,
|
||||||
|
|
||||||
|
async(ProfileAction.ComponentLifecycle::class) {
|
||||||
|
when (it) {
|
||||||
|
ProfileAction.ComponentLifecycle.Visible -> {
|
||||||
|
jobBag.replace(Page.Profile::class, profileUseCase.content().onEach { content ->
|
||||||
|
withPageContext<Page.Profile> {
|
||||||
|
pageDispatch(PageStateChange.UpdatePage(it.copy(content = content)))
|
||||||
|
}
|
||||||
|
}.launchIn(coroutineScope))
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileAction.ComponentLifecycle.Gone -> jobBag.cancelAll()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async(ProfileAction.GoToInvitations::class) {
|
||||||
|
dispatch(PageAction.GoTo(SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading()))))
|
||||||
|
|
||||||
|
jobBag.replace(Page.Invitations::class, chatEngine.invites()
|
||||||
|
.onEach { invitations ->
|
||||||
|
withPageContext<Page.Invitations> {
|
||||||
|
pageDispatch(PageStateChange.UpdatePage(it.copy(content = Lce.Content(invitations))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(coroutineScope))
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
async(ProfileAction.Reset::class) {
|
||||||
|
when (rawPage().state) {
|
||||||
|
is Page.Invitations -> {
|
||||||
|
dispatch(PageAction.GoTo(SpiderPage<Page>(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false)))
|
||||||
|
}
|
||||||
|
|
||||||
|
is Page.Profile -> {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sideEffect(ProfileAction.AcceptRoomInvite::class) { action, _ ->
|
||||||
|
kotlin.runCatching { chatEngine.joinRoom(action.roomId) }.fold(
|
||||||
|
onFailure = { errorTracker.track(it) },
|
||||||
|
onSuccess = {}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
sideEffect(ProfileAction.RejectRoomInvite::class) { action, _ ->
|
||||||
|
kotlin.runCatching { chatEngine.rejectJoinRoom(action.roomId) }.fold(
|
||||||
|
onFailure = { errorTracker.track(it) },
|
||||||
|
onSuccess = {}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
sideEffect(PageStateChange.ChangePage::class) { action, _ ->
|
||||||
|
jobBag.cancel(action.previous.state::class)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
|
@ -1,14 +1,14 @@
|
||||||
package app.dapk.st.profile
|
package app.dapk.st.profile.state
|
||||||
|
|
||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
|
import app.dapk.st.core.State
|
||||||
|
import app.dapk.st.core.page.PageContainer
|
||||||
import app.dapk.st.design.components.Route
|
import app.dapk.st.design.components.Route
|
||||||
import app.dapk.st.design.components.SpiderPage
|
|
||||||
import app.dapk.st.engine.Me
|
import app.dapk.st.engine.Me
|
||||||
import app.dapk.st.engine.RoomInvite
|
import app.dapk.st.engine.RoomInvite
|
||||||
|
import app.dapk.state.Combined2
|
||||||
|
|
||||||
data class ProfileScreenState(
|
typealias ProfileState = State<Combined2<PageContainer<Page>, Unit>, Unit>
|
||||||
val page: SpiderPage<out Page>,
|
|
||||||
)
|
|
||||||
|
|
||||||
sealed interface Page {
|
sealed interface Page {
|
||||||
data class Profile(val content: Lce<Content>) : Page {
|
data class Profile(val content: Lce<Content>) : Page {
|
||||||
|
@ -25,8 +25,3 @@ sealed interface Page {
|
||||||
val invitation = Route<Invitations>("Invitations")
|
val invitation = Route<Invitations>("Invitations")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ProfileEvent {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package app.dapk.st.profile.state
|
||||||
|
|
||||||
|
import app.dapk.st.core.Lce
|
||||||
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
|
import app.dapk.st.engine.ChatEngine
|
||||||
|
import app.dapk.st.engine.Me
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class ProfileUseCase(
|
||||||
|
private val chatEngine: ChatEngine,
|
||||||
|
private val errorTracker: ErrorTracker,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var meCache: Me? = null
|
||||||
|
|
||||||
|
fun content(): Flow<Lce<Page.Profile.Content>> {
|
||||||
|
return combine(fetchMe(), chatEngine.invites(), transform = { me, invites -> me to invites }).map { (me, invites) ->
|
||||||
|
when (me.isSuccess) {
|
||||||
|
true -> Lce.Content(Page.Profile.Content(me.getOrThrow(), invites.size))
|
||||||
|
false -> Lce.Error(me.exceptionOrNull()!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchMe() = flow {
|
||||||
|
meCache?.let { emit(Result.success(it)) }
|
||||||
|
val result = runCatching { chatEngine.me(forceRefresh = true) }
|
||||||
|
.onFailure { errorTracker.track(it, "Loading profile") }
|
||||||
|
.onSuccess { meCache = it }
|
||||||
|
emit(result)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package app.dapk.st.profile.state
|
||||||
|
|
||||||
|
import app.dapk.st.core.Lce
|
||||||
|
import app.dapk.st.core.page.PageAction
|
||||||
|
import app.dapk.st.core.page.PageContainer
|
||||||
|
import app.dapk.st.core.page.PageStateChange
|
||||||
|
import app.dapk.st.design.components.SpiderPage
|
||||||
|
import app.dapk.st.engine.Me
|
||||||
|
import app.dapk.st.matrix.common.HomeServerUrl
|
||||||
|
import app.dapk.state.Combined2
|
||||||
|
import fake.FakeChatEngine
|
||||||
|
import fake.FakeErrorTracker
|
||||||
|
import fake.FakeJobBag
|
||||||
|
import fixture.aRoomId
|
||||||
|
import fixture.aRoomInvite
|
||||||
|
import fixture.aUserId
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.Test
|
||||||
|
import test.assertOnlyDispatches
|
||||||
|
import test.delegateEmit
|
||||||
|
import test.testReducer
|
||||||
|
|
||||||
|
private val INITIAL_PROFILE_PAGE = SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false)
|
||||||
|
private val INITIAL_INVITATION_PAGE = SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading()), hasToolbar = true)
|
||||||
|
private val A_ROOM_ID = aRoomId()
|
||||||
|
private val A_PROFILE_CONTENT = Page.Profile.Content(Me(aUserId(), null, null, HomeServerUrl("ignored")), invitationsCount = 4)
|
||||||
|
|
||||||
|
class ProfileReducerTest {
|
||||||
|
|
||||||
|
private val fakeChatEngine = FakeChatEngine()
|
||||||
|
private val fakeErrorTracker = FakeErrorTracker()
|
||||||
|
private val fakeProfileUseCase = FakeProfileUseCase()
|
||||||
|
private val fakeJobBag = FakeJobBag()
|
||||||
|
|
||||||
|
private val runReducerTest = testReducer { _: (Unit) -> Unit ->
|
||||||
|
profileReducer(
|
||||||
|
fakeChatEngine,
|
||||||
|
fakeErrorTracker,
|
||||||
|
fakeProfileUseCase.instance,
|
||||||
|
fakeJobBag.instance,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state is empty loading`() = runReducerTest {
|
||||||
|
assertInitialState(pageState(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given on Profile page, when Reset, then does nothing`() = runReducerTest {
|
||||||
|
reduce(ProfileAction.Reset)
|
||||||
|
|
||||||
|
assertNoChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when Visible, then updates Profile page content`() = runReducerTest {
|
||||||
|
fakeJobBag.instance.expect { it.replace(Page.Profile::class, any()) }
|
||||||
|
fakeProfileUseCase.givenContent().emits(Lce.Content(A_PROFILE_CONTENT))
|
||||||
|
|
||||||
|
reduce(ProfileAction.ComponentLifecycle.Visible)
|
||||||
|
|
||||||
|
assertOnlyDispatches(
|
||||||
|
PageStateChange.UpdatePage(INITIAL_PROFILE_PAGE.state.copy(Lce.Content(A_PROFILE_CONTENT))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when GoToInvitations, then goes to Invitations page and updates content`() = runReducerTest {
|
||||||
|
fakeJobBag.instance.expect { it.replace(Page.Invitations::class, any()) }
|
||||||
|
val goToInvitations = PageAction.GoTo(INITIAL_INVITATION_PAGE)
|
||||||
|
actionSideEffect(goToInvitations) { pageState(goToInvitations.page) }
|
||||||
|
val content = listOf(aRoomInvite())
|
||||||
|
fakeChatEngine.givenInvites().emits(content)
|
||||||
|
|
||||||
|
reduce(ProfileAction.GoToInvitations)
|
||||||
|
|
||||||
|
assertOnlyDispatches(
|
||||||
|
PageAction.GoTo(INITIAL_INVITATION_PAGE),
|
||||||
|
PageStateChange.UpdatePage(INITIAL_INVITATION_PAGE.state.copy(Lce.Content(content))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given on Invitation page, when Reset, then goes to Profile page`() = runReducerTest {
|
||||||
|
setState(pageState(INITIAL_INVITATION_PAGE))
|
||||||
|
|
||||||
|
reduce(ProfileAction.Reset)
|
||||||
|
|
||||||
|
assertOnlyDispatches(PageAction.GoTo(INITIAL_PROFILE_PAGE))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when RejectRoomInvite, then rejects room`() = runReducerTest {
|
||||||
|
fakeChatEngine.expect { it.rejectJoinRoom(A_ROOM_ID) }
|
||||||
|
|
||||||
|
reduce(ProfileAction.RejectRoomInvite(A_ROOM_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when AcceptRoomInvite, then joins room`() = runReducerTest {
|
||||||
|
fakeChatEngine.expect { it.joinRoom(A_ROOM_ID) }
|
||||||
|
|
||||||
|
reduce(ProfileAction.AcceptRoomInvite(A_ROOM_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when ChangePage, then cancels any previous page jobs`() = runReducerTest {
|
||||||
|
fakeJobBag.instance.expect { it.cancel(Page.Invitations::class) }
|
||||||
|
|
||||||
|
reduce(PageStateChange.ChangePage(INITIAL_INVITATION_PAGE, INITIAL_PROFILE_PAGE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <P> pageState(page: SpiderPage<out P>) = Combined2(PageContainer(page), Unit)
|
||||||
|
|
||||||
|
class FakeProfileUseCase {
|
||||||
|
val instance = mockk<ProfileUseCase>()
|
||||||
|
fun givenContent() = every { instance.content() }.delegateEmit()
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package app.dapk.st.profile.state
|
||||||
|
|
||||||
|
import app.dapk.st.core.Lce
|
||||||
|
import app.dapk.st.engine.Me
|
||||||
|
import app.dapk.st.matrix.common.HomeServerUrl
|
||||||
|
import fake.FakeChatEngine
|
||||||
|
import fake.FakeErrorTracker
|
||||||
|
import fixture.aRoomInvite
|
||||||
|
import fixture.aUserId
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private val A_ME = Me(aUserId(), null, null, HomeServerUrl("ignored"))
|
||||||
|
private val AN_INVITES_LIST = listOf(aRoomInvite(), aRoomInvite(), aRoomInvite(), aRoomInvite())
|
||||||
|
private val AN_ERROR = RuntimeException()
|
||||||
|
|
||||||
|
class ProfileUseCaseTest {
|
||||||
|
|
||||||
|
private val fakeChatEngine = FakeChatEngine()
|
||||||
|
private val fakeErrorTracker = FakeErrorTracker()
|
||||||
|
|
||||||
|
private val useCase = ProfileUseCase(fakeChatEngine, fakeErrorTracker)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given me and invites, when fetching content, then emits content`() = runTest {
|
||||||
|
fakeChatEngine.givenMe(forceRefresh = true).returns(A_ME)
|
||||||
|
fakeChatEngine.givenInvites().emits(AN_INVITES_LIST)
|
||||||
|
|
||||||
|
val result = useCase.content().first()
|
||||||
|
|
||||||
|
result shouldBeEqualTo Lce.Content(Page.Profile.Content(A_ME, invitationsCount = AN_INVITES_LIST.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given me fails, when fetching content, then emits error`() = runTest {
|
||||||
|
fakeChatEngine.givenMe(forceRefresh = true).throws(AN_ERROR)
|
||||||
|
fakeChatEngine.givenInvites().emits(emptyList())
|
||||||
|
|
||||||
|
val result = useCase.content().first()
|
||||||
|
|
||||||
|
result shouldBeEqualTo Lce.Error(AN_ERROR)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue