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() {
|
||||
|
||||
fun givenMessages(roomId: RoomId, disableReadReceipts: Boolean) = every { messages(roomId, disableReadReceipts) }.delegateReturn()
|
||||
|
||||
fun givenDirectory() = every { directory() }.delegateReturn()
|
||||
|
||||
fun givenImportKeys(inputStream: InputStream, passphrase: String) = coEvery { inputStream.importRoomKeys(passphrase) }.delegateReturn()
|
||||
|
||||
fun givenNotificationsInvites() = every { notificationsInvites() }.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()
|
||||
}
|
||||
|
||||
fun cancelAll() {
|
||||
jobs.values.forEach { it.cancel() }
|
||||
}
|
||||
|
||||
}
|
|
@ -3,7 +3,6 @@ applyAndroidComposeLibraryModule(project)
|
|||
dependencies {
|
||||
implementation project(":chat-engine")
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(":domains:state")
|
||||
implementation project(":features:messenger")
|
||||
implementation project(":core")
|
||||
|
@ -16,7 +15,6 @@ dependencies {
|
|||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:state"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||
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.engine.ChatEngine
|
||||
import app.dapk.st.login.LoginViewModel
|
||||
import app.dapk.st.profile.ProfileViewModel
|
||||
import app.dapk.st.profile.state.ProfileState
|
||||
|
||||
class HomeModule(
|
||||
private val chatEngine: ChatEngine,
|
||||
|
@ -13,13 +13,13 @@ class HomeModule(
|
|||
val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
||||
) : ProvidableModule {
|
||||
|
||||
internal fun homeViewModel(directory: DirectoryState, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel {
|
||||
internal fun homeViewModel(directory: DirectoryState, login: LoginViewModel, profile: ProfileState): HomeViewModel {
|
||||
return HomeViewModel(
|
||||
chatEngine,
|
||||
storeModule.credentialsStore(),
|
||||
directory,
|
||||
login,
|
||||
profileViewModel,
|
||||
profile,
|
||||
storeModule.cacheCleaner(),
|
||||
betaVersionUpgradeUseCase,
|
||||
)
|
||||
|
|
|
@ -10,7 +10,8 @@ import app.dapk.st.home.HomeScreenState.*
|
|||
import app.dapk.st.login.LoginViewModel
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
|
@ -24,7 +25,7 @@ internal class HomeViewModel(
|
|||
private val credentialsProvider: CredentialsStore,
|
||||
private val directoryState: DirectoryState,
|
||||
private val loginViewModel: LoginViewModel,
|
||||
private val profileViewModel: ProfileViewModel,
|
||||
private val profileState: ProfileState,
|
||||
private val cacheCleaner: StoreCleaner,
|
||||
private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
||||
) : DapkViewModel<HomeScreenState, HomeEvent>(
|
||||
|
@ -35,7 +36,7 @@ internal class HomeViewModel(
|
|||
|
||||
fun directory() = directoryState
|
||||
fun login() = loginViewModel
|
||||
fun profile() = profileViewModel
|
||||
fun profile() = profileState
|
||||
|
||||
fun start() {
|
||||
viewModelScope.launch {
|
||||
|
@ -125,7 +126,7 @@ internal class HomeViewModel(
|
|||
|
||||
Page.Profile -> {
|
||||
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 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) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
|
@ -4,8 +4,17 @@ dependencies {
|
|||
implementation project(":chat-engine")
|
||||
implementation project(":features:settings")
|
||||
implementation project(':domains:store')
|
||||
implementation project(':domains:state')
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(":design-library")
|
||||
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
|
||||
|
||||
import app.dapk.st.core.JobBag
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.core.createStateViewModel
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
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(
|
||||
private val chatEngine: ChatEngine,
|
||||
private val errorTracker: ErrorTracker,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun profileViewModel(): ProfileViewModel {
|
||||
return ProfileViewModel(chatEngine, errorTracker)
|
||||
fun profileState(): ProfileState {
|
||||
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.LifecycleEffect
|
||||
import app.dapk.st.core.components.CenteredLoading
|
||||
import app.dapk.st.core.page.PageAction
|
||||
import app.dapk.st.design.components.*
|
||||
import app.dapk.st.engine.RoomInvite
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun ProfileScreen(viewModel: ProfileViewModel, onTopLevelBack: () -> Unit) {
|
||||
fun ProfileScreen(viewModel: ProfileState, onTopLevelBack: () -> Unit) {
|
||||
viewModel.ObserveEvents()
|
||||
|
||||
LifecycleEffect(
|
||||
onStart = { viewModel.start() },
|
||||
onStop = { viewModel.stop() }
|
||||
onStart = { viewModel.dispatch(ProfileAction.ComponentLifecycle.Visible) },
|
||||
onStop = { viewModel.dispatch(ProfileAction.ComponentLifecycle.Gone) }
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
|
@ -39,11 +43,11 @@ fun ProfileScreen(viewModel: ProfileViewModel, onTopLevelBack: () -> Unit) {
|
|||
val onNavigate: (SpiderPage<out Page>?) -> Unit = {
|
||||
when (it) {
|
||||
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) {
|
||||
ProfilePage(context, viewModel, it)
|
||||
}
|
||||
|
@ -54,7 +58,7 @@ fun ProfileScreen(viewModel: ProfileViewModel, onTopLevelBack: () -> Unit) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile: Page.Profile) {
|
||||
private fun ProfilePage(context: Context, viewModel: ProfileState, profile: Page.Profile) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
@ -67,7 +71,7 @@ private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile:
|
|||
|
||||
when (val state = profile.content) {
|
||||
is Lce.Loading -> CenteredLoading()
|
||||
is Lce.Error -> GenericError { viewModel.start() }
|
||||
is Lce.Error -> GenericError { viewModel.dispatch(ProfileAction.ComponentLifecycle.Visible) }
|
||||
is Lce.Content -> {
|
||||
val configuration = LocalConfiguration.current
|
||||
val content = state.value
|
||||
|
@ -111,7 +115,7 @@ private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile:
|
|||
TextRow(
|
||||
title = "Invitations",
|
||||
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
|
||||
private fun SpiderItemScope.Invitations(viewModel: ProfileViewModel, invitations: Page.Invitations) {
|
||||
private fun SpiderItemScope.Invitations(viewModel: ProfileState, invitations: Page.Invitations) {
|
||||
when (val state = invitations.content) {
|
||||
is Lce.Loading -> CenteredLoading()
|
||||
is Lce.Content -> {
|
||||
|
@ -133,11 +137,11 @@ private fun SpiderItemScope.Invitations(viewModel: ProfileViewModel, invitations
|
|||
TextRow(title = text, includeDivider = false) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
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())
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
@Composable
|
||||
private fun ProfileViewModel.ObserveEvents() {
|
||||
private fun ProfileState.ObserveEvents() {
|
||||
// StartObserving {
|
||||
// this@ObserveEvents.events.launch {
|
||||
// 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.State
|
||||
import app.dapk.st.core.page.PageContainer
|
||||
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.RoomInvite
|
||||
import app.dapk.state.Combined2
|
||||
|
||||
data class ProfileScreenState(
|
||||
val page: SpiderPage<out Page>,
|
||||
)
|
||||
typealias ProfileState = State<Combined2<PageContainer<Page>, Unit>, Unit>
|
||||
|
||||
sealed interface Page {
|
||||
data class Profile(val content: Lce<Content>) : Page {
|
||||
|
@ -25,8 +25,3 @@ sealed interface Page {
|
|||
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