Merge branch 'develop' of https://github.com/vector-im/element-x-android into dla/feature/custom_room_notification_settings_list
This commit is contained in:
commit
87b8bfe99d
1981 changed files with 15504 additions and 5063 deletions
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
package io.element.android.appnav
|
||||
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.composable.PermanentChild
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
|
|
@ -48,14 +50,18 @@ import io.element.android.features.ftue.api.state.FtueState
|
|||
import io.element.android.features.invitelist.api.InviteListEntryPoint
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.features.lockscreen.api.LockScreenState
|
||||
import io.element.android.features.lockscreen.api.LockScreenStateService
|
||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.waitForChildAttached
|
||||
import io.element.android.libraries.deeplink.DeeplinkData
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||
|
|
@ -68,6 +74,7 @@ import kotlinx.coroutines.FlowPreview
|
|||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -86,6 +93,8 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
private val networkMonitor: NetworkMonitor,
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
private val ftueState: FtueState,
|
||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||
private val lockScreenStateService: LockScreenStateService,
|
||||
private val matrixClient: MatrixClient,
|
||||
snackbarDispatcher: SnackbarDispatcher,
|
||||
) : BackstackNode<LoggedInFlowNode.NavTarget>(
|
||||
|
|
@ -93,6 +102,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
initialElement = NavTarget.RoomList,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
permanentNavModel = PermanentNavModel(
|
||||
navTargets = setOf(NavTarget.LoggedInPermanent, NavTarget.LockPermanent),
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
|
|
@ -121,9 +134,19 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
backstack.push(NavTarget.Ftue)
|
||||
}
|
||||
},
|
||||
onStop = {
|
||||
//Counterpart startSync is done in observeSyncStateAndNetworkStatus method.
|
||||
onResume = {
|
||||
coroutineScope.launch {
|
||||
lockScreenStateService.entersForeground()
|
||||
}
|
||||
},
|
||||
onPause = {
|
||||
coroutineScope.launch {
|
||||
lockScreenStateService.entersBackground()
|
||||
}
|
||||
},
|
||||
onStop = {
|
||||
coroutineScope.launch {
|
||||
//Counterpart startSync is done in observeSyncStateAndNetworkStatus method.
|
||||
syncService.stopSync()
|
||||
}
|
||||
},
|
||||
|
|
@ -159,7 +182,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object Permanent : NavTarget
|
||||
data object LoggedInPermanent : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object LockPermanent : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object RoomList : NavTarget
|
||||
|
|
@ -188,9 +214,12 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Permanent -> {
|
||||
NavTarget.LoggedInPermanent -> {
|
||||
createNode<LoggedInNode>(buildContext)
|
||||
}
|
||||
NavTarget.LockPermanent -> {
|
||||
lockScreenEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
NavTarget.RoomList -> {
|
||||
val callback = object : RoomListEntryPoint.Callback {
|
||||
override fun onRoomClicked(roomId: RoomId) {
|
||||
|
|
@ -308,30 +337,37 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = Modifier,
|
||||
// Animate navigation to settings and to a room
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
|
||||
val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState()
|
||||
|
||||
if (!isFtueDisplayed) {
|
||||
PermanentChild(navTarget = NavTarget.Permanent)
|
||||
}
|
||||
internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) = withContext(lifecycleScope.coroutineContext) {
|
||||
notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
|
||||
backstack.singleTop(NavTarget.RoomList)
|
||||
backstack.push(NavTarget.InviteList)
|
||||
waitForChildAttached<Node, NavTarget> { navTarget ->
|
||||
navTarget is NavTarget.InviteList
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun attachRoom(deeplinkData: DeeplinkData.Room) {
|
||||
backstack.push(NavTarget.Room(deeplinkData.roomId))
|
||||
}
|
||||
|
||||
internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) {
|
||||
notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
|
||||
backstack.push(NavTarget.InviteList)
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
val lockScreenState by lockScreenStateService.state.collectAsState()
|
||||
when (lockScreenState) {
|
||||
LockScreenState.Unlocked -> {
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = Modifier,
|
||||
// Animate navigation to settings and to a room
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState()
|
||||
if (!isFtueDisplayed) {
|
||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
||||
}
|
||||
}
|
||||
LockScreenState.Locked -> {
|
||||
MoveActivityToBackgroundBackHandler()
|
||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.appnav
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@Composable
|
||||
fun MoveActivityToBackgroundBackHandler(enabled: Boolean = true) {
|
||||
|
||||
fun Context.findActivity(): ComponentActivity? = when (this) {
|
||||
is ComponentActivity -> this
|
||||
is ContextWrapper -> baseContext.findActivity()
|
||||
else -> null
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
BackHandler(enabled = enabled) {
|
||||
context.findActivity()?.moveTaskToBack(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ import io.element.android.appnav.root.RootView
|
|||
import io.element.android.features.login.api.oidc.OidcAction
|
||||
import io.element.android.features.login.api.oidc.OidcActionFlow
|
||||
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
||||
import io.element.android.features.signedout.api.SignedOutEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -54,6 +55,7 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
|
|||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -69,6 +71,7 @@ class RootFlowNode @AssistedInject constructor(
|
|||
private val matrixClientsHolder: MatrixClientsHolder,
|
||||
private val presenter: RootPresenter,
|
||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||
private val signedOutEntryPoint: SignedOutEntryPoint,
|
||||
private val intentResolver: IntentResolver,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
) : BackstackNode<RootFlowNode.NavTarget>(
|
||||
|
|
@ -97,13 +100,20 @@ class RootFlowNode @AssistedInject constructor(
|
|||
.distinctUntilChanged()
|
||||
.onEach { navState ->
|
||||
Timber.v("navState=$navState")
|
||||
if (navState.isLoggedIn) {
|
||||
tryToRestoreLatestSession(
|
||||
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
|
||||
onFailure = { switchToNotLoggedInFlow() }
|
||||
)
|
||||
} else {
|
||||
switchToNotLoggedInFlow()
|
||||
when (navState.loggedInState) {
|
||||
is LoggedInState.LoggedIn -> {
|
||||
if (navState.loggedInState.isTokenValid) {
|
||||
tryToRestoreLatestSession(
|
||||
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
|
||||
onFailure = { switchToNotLoggedInFlow() }
|
||||
)
|
||||
} else {
|
||||
switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
|
||||
}
|
||||
}
|
||||
LoggedInState.NotLoggedIn -> {
|
||||
switchToNotLoggedInFlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
|
@ -118,6 +128,10 @@ class RootFlowNode @AssistedInject constructor(
|
|||
backstack.safeRoot(NavTarget.NotLoggedInFlow)
|
||||
}
|
||||
|
||||
private fun switchToSignedOutFlow(sessionId: SessionId) {
|
||||
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
|
||||
}
|
||||
|
||||
private suspend fun restoreSessionIfNeeded(
|
||||
sessionId: SessionId,
|
||||
onFailure: () -> Unit = {},
|
||||
|
|
@ -179,6 +193,11 @@ class RootFlowNode @AssistedInject constructor(
|
|||
val navId: Int
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class SignedOutFlow(
|
||||
val sessionId: SessionId
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object BugReport : NavTarget
|
||||
}
|
||||
|
|
@ -198,6 +217,15 @@ class RootFlowNode @AssistedInject constructor(
|
|||
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
||||
}
|
||||
NavTarget.NotLoggedInFlow -> createNode<NotLoggedInFlowNode>(buildContext)
|
||||
is NavTarget.SignedOutFlow -> {
|
||||
signedOutEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(
|
||||
SignedOutEntryPoint.Params(
|
||||
sessionId = navTarget.sessionId
|
||||
)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
NavTarget.SplashScreen -> splashNode(buildContext)
|
||||
NavTarget.BugReport -> {
|
||||
val callback = object : BugReportEntryPoint.Callback {
|
||||
|
|
@ -234,7 +262,7 @@ class RootFlowNode @AssistedInject constructor(
|
|||
.apply {
|
||||
when (deeplinkData) {
|
||||
is DeeplinkData.Root -> attachRoot()
|
||||
is DeeplinkData.Room -> attachRoom(deeplinkData)
|
||||
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
|
||||
is DeeplinkData.InviteList -> attachInviteList(deeplinkData)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,10 @@ class LoggedInNode @AssistedInject constructor(
|
|||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val loggedInPresenter: LoggedInPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
) : Node(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
||||
@Composable
|
||||
|
|
@ -47,7 +47,7 @@ fun LoggedInView(
|
|||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview {
|
||||
LoggedInView(
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
|
|
@ -81,7 +81,7 @@ fun SyncStateView(
|
|||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SyncStateViewPreview() = ElementPreview {
|
||||
// Add a box to see the shadow
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorVi
|
|||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
|
|
@ -100,7 +100,7 @@ private fun LoadingRoomTopBar(
|
|||
)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview {
|
||||
LoadingRoomNodeView(
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -60,6 +61,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
|||
private val messagesEntryPoint: MessagesEntryPoint,
|
||||
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
roomComponentFactory: RoomComponentFactory,
|
||||
roomMembershipObserver: RoomMembershipObserver,
|
||||
) : BackstackNode<RoomLoadedFlowNode.NavTarget>(
|
||||
|
|
@ -91,6 +93,16 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
|||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||
fetchRoomMembers()
|
||||
},
|
||||
onResume = {
|
||||
appCoroutineScope.launch {
|
||||
inputs.room.subscribeToSync()
|
||||
}
|
||||
},
|
||||
onPause = {
|
||||
appCoroutineScope.launch {
|
||||
inputs.room.unsubscribeFromSync()
|
||||
}
|
||||
},
|
||||
onDestroy = {
|
||||
Timber.v("OnDestroy")
|
||||
appNavigationStateService.onLeavingRoom(id)
|
||||
|
|
@ -169,9 +181,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
|||
// because this node enters 'onDestroy' before his children, so it can leads to
|
||||
// using the room in a child node where it's already closed.
|
||||
DisposableEffect(Unit) {
|
||||
inputs.room.subscribeToSync()
|
||||
onDispose {
|
||||
inputs.room.unsubscribeFromSync()
|
||||
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
||||
inputs.room.destroy()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package io.element.android.appnav.root
|
||||
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
|
||||
/**
|
||||
* [RootNavState] produced by [RootNavStateFlowFactory].
|
||||
*/
|
||||
|
|
@ -26,7 +28,7 @@ data class RootNavState(
|
|||
*/
|
||||
val cacheIndex: Int,
|
||||
/**
|
||||
* true if we are currently loggedIn.
|
||||
* LoggedInState.
|
||||
*/
|
||||
val isLoggedIn: Boolean
|
||||
val loggedInState: LoggedInState,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ import io.element.android.appnav.di.MatrixClientsHolder
|
|||
import io.element.android.features.login.api.LoginUserStory
|
||||
import io.element.android.features.preferences.api.CacheService
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
|
@ -47,9 +47,14 @@ class RootNavStateFlowFactory @Inject constructor(
|
|||
fun create(savedStateMap: SavedStateMap?): Flow<RootNavState> {
|
||||
return combine(
|
||||
cacheIndexFlow(savedStateMap),
|
||||
isUserLoggedInFlow(),
|
||||
) { cacheIndex, isLoggedIn ->
|
||||
RootNavState(cacheIndex = cacheIndex, isLoggedIn = isLoggedIn)
|
||||
authenticationService.loggedInStateFlow(),
|
||||
loginUserStory.loginFlowIsDone,
|
||||
) { cacheIndex, loggedInState, loginFlowIsDone ->
|
||||
if (loginFlowIsDone) {
|
||||
RootNavState(cacheIndex = cacheIndex, loggedInState = loggedInState)
|
||||
} else {
|
||||
RootNavState(cacheIndex = cacheIndex, loggedInState = LoggedInState.NotLoggedIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,16 +77,6 @@ class RootNavStateFlowFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun isUserLoggedInFlow(): Flow<Boolean> {
|
||||
return combine(
|
||||
authenticationService.isLoggedIn(),
|
||||
loginUserStory.loginFlowIsDone
|
||||
) { isLoggedIn, loginFlowIsDone ->
|
||||
isLoggedIn && loginFlowIsDone
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a flow of integer that increments the value by one each time a new element is emitted upstream.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import io.element.android.features.rageshake.api.crash.CrashDetectionEvents
|
|||
import io.element.android.features.rageshake.api.crash.CrashDetectionView
|
||||
import io.element.android.features.rageshake.api.detection.RageshakeDetectionEvents
|
||||
import io.element.android.features.rageshake.api.detection.RageshakeDetectionView
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.services.apperror.impl.AppErrorView
|
||||
|
|
@ -66,7 +66,7 @@ fun RootView(
|
|||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RootPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreview {
|
||||
RootView(rootState) {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -85,6 +87,7 @@ class RoomFlowNodeTest {
|
|||
plugins: List<Plugin>,
|
||||
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||
coroutineScope: CoroutineScope,
|
||||
) = RoomLoadedFlowNode(
|
||||
buildContext = BuildContext.root(savedStateMap = null),
|
||||
plugins = plugins,
|
||||
|
|
@ -92,16 +95,21 @@ class RoomFlowNodeTest {
|
|||
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
||||
appNavigationStateService = FakeAppNavigationStateService(),
|
||||
roomMembershipObserver = RoomMembershipObserver(),
|
||||
appCoroutineScope = coroutineScope,
|
||||
roomComponentFactory = FakeRoomComponentFactory(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `given a room flow node when initialized then it loads messages entry point`() {
|
||||
fun `given a room flow node when initialized then it loads messages entry point`() = runTest {
|
||||
// GIVEN
|
||||
val room = FakeMatrixRoom()
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val inputs = RoomLoadedFlowNode.Inputs(room)
|
||||
val roomFlowNode = aRoomFlowNode(listOf(inputs), fakeMessagesEntryPoint)
|
||||
val roomFlowNode = aRoomFlowNode(
|
||||
plugins = listOf(inputs),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
coroutineScope = this
|
||||
)
|
||||
// WHEN
|
||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||
|
||||
|
|
@ -113,13 +121,18 @@ class RoomFlowNodeTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() {
|
||||
fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest {
|
||||
// GIVEN
|
||||
val room = FakeMatrixRoom()
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||
val inputs = RoomLoadedFlowNode.Inputs(room)
|
||||
val roomFlowNode = aRoomFlowNode(listOf(inputs), fakeMessagesEntryPoint, fakeRoomDetailsEntryPoint)
|
||||
val roomFlowNode = aRoomFlowNode(
|
||||
plugins = listOf(inputs),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||
coroutineScope = this
|
||||
)
|
||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||
// WHEN
|
||||
fakeMessagesEntryPoint.callback?.onRoomDetailsClicked()
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class RootPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
val presenter = createRootPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -54,7 +54,7 @@ class RootPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - passes app error state`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
val presenter = createRootPresenter(
|
||||
appErrorService = DefaultAppErrorStateService().apply {
|
||||
showError("Bad news", "Something bad happened")
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ class RootPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
private fun createRootPresenter(
|
||||
appErrorService: AppErrorStateService = DefaultAppErrorStateService()
|
||||
): RootPresenter {
|
||||
val crashDataStore = FakeCrashDataStore()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.appnav.di
|
||||
|
||||
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class MatrixClientsHolderTest {
|
||||
@Test
|
||||
fun `test getOrNull`() {
|
||||
val fakeAuthenticationService = FakeAuthenticationService()
|
||||
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test getOrRestore`() = runTest {
|
||||
val fakeAuthenticationService = FakeAuthenticationService()
|
||||
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||
val fakeMatrixClient = FakeMatrixClient()
|
||||
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
||||
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
|
||||
assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
|
||||
// Do it again to it the cache
|
||||
assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
|
||||
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test remove`() = runTest {
|
||||
val fakeAuthenticationService = FakeAuthenticationService()
|
||||
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||
val fakeMatrixClient = FakeMatrixClient()
|
||||
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
||||
assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
|
||||
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
|
||||
// Remove
|
||||
matrixClientsHolder.remove(A_SESSION_ID)
|
||||
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test remove all`() = runTest {
|
||||
val fakeAuthenticationService = FakeAuthenticationService()
|
||||
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||
val fakeMatrixClient = FakeMatrixClient()
|
||||
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
||||
assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
|
||||
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
|
||||
// Remove all
|
||||
matrixClientsHolder.removeAll()
|
||||
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test save and restore`() = runTest {
|
||||
val fakeAuthenticationService = FakeAuthenticationService()
|
||||
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||
val fakeMatrixClient = FakeMatrixClient()
|
||||
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
||||
matrixClientsHolder.getOrRestore(A_SESSION_ID)
|
||||
val savedStateMap = MutableSavedStateMapImpl { true }
|
||||
matrixClientsHolder.saveIntoSavedState(savedStateMap)
|
||||
assertThat(savedStateMap.size).isEqualTo(1)
|
||||
// Test Restore with non-empty map
|
||||
matrixClientsHolder.restoreWithSavedState(savedStateMap)
|
||||
// Empty the map
|
||||
matrixClientsHolder.removeAll()
|
||||
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
|
||||
// Restore again
|
||||
matrixClientsHolder.restoreWithSavedState(savedStateMap)
|
||||
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ class LoggedInPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
val presenter = createLoggedInPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -54,7 +54,7 @@ class LoggedInPresenterTest {
|
|||
@Test
|
||||
fun `present - show sync spinner`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
val presenter = createPresenter(roomListService, NetworkStatus.Online)
|
||||
val presenter = createLoggedInPresenter(roomListService, NetworkStatus.Online)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -66,7 +66,7 @@ class LoggedInPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
private fun createLoggedInPresenter(
|
||||
roomListService: RoomListService = FakeRoomListService(),
|
||||
networkStatus: NetworkStatus = NetworkStatus.Offline
|
||||
): LoggedInPresenter {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue