Fix navigation issue.

Ensure that the timeout has effect only in Idle state.
This commit is contained in:
Benoit Marty 2026-04-13 15:25:37 +02:00
parent 73e1a092d2
commit f5e1cbef38
2 changed files with 59 additions and 57 deletions

View file

@ -12,67 +12,60 @@ import io.element.android.features.login.impl.classic.ElementClassicConnection
import io.element.android.features.login.impl.classic.ElementClassicConnectionState
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.api.toUserListFlow
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.flowOf
@Inject
class ClassicFlowNodeHelper(
private val elementClassicConnection: ElementClassicConnection,
private val sessionStore: SessionStore,
) {
// Ensure user is not stuck on the loading screen.
// If Element Classic is taking too long to communicate (or crashes), unblock the user after a few seconds.
private val timeoutFLow = flow {
emit(false)
delay(5_000)
emit(true)
}
@OptIn(ExperimentalCoroutinesApi::class)
fun navigationEventFlow(): Flow<NavigationEvent> {
return combine(
timeoutFLow,
elementClassicConnection.stateFlow
.distinctUntilChangedBy {
// Ignore change on ElementClassicConnectionState.ElementClassicReady.avatar
if (it is ElementClassicConnectionState.ElementClassicReady) {
it.copy(avatar = null)
} else {
it
}
},
sessionStore.sessionsFlow().toUserListFlow()
// Take only 1 emission of the sessions, else when the user actually logged in it will trigger a navigation to OnBoarding.
.take(1),
) { timeout, elementClassicConnectionState, existingSessions ->
when (elementClassicConnectionState) {
ElementClassicConnectionState.Idle -> {
if (timeout) {
NavigationEvent.NavigateToOnBoarding
} else {
NavigationEvent.Idle
}
return elementClassicConnection.stateFlow
.distinctUntilChangedBy {
// Ignore change on ElementClassicConnectionState.ElementClassicReady.avatar
if (it is ElementClassicConnectionState.ElementClassicReady) {
it.copy(avatar = null)
} else {
it
}
ElementClassicConnectionState.ElementClassicNotFound,
ElementClassicConnectionState.ElementClassicReadyNoSession,
is ElementClassicConnectionState.Error -> {
NavigationEvent.NavigateToOnBoarding
}
is ElementClassicConnectionState.ElementClassicReady -> {
if (elementClassicConnectionState.elementClassicSession.userId.value in existingSessions) {
NavigationEvent.NavigateToOnBoarding
} else {
// 2 cases when this can be run:
// First time this screen will be displayed
// Missing key backup screen was displayed, but the data has changed (user set up the key backup on Classic),
// and the app is resuming.
NavigationEvent.NavigateToLoginWithClassic(elementClassicConnectionState.elementClassicSession.userId)
}
.flatMapLatest { elementClassicConnectionState ->
when (elementClassicConnectionState) {
ElementClassicConnectionState.Idle -> {
// Ensure user is not stuck on the loading screen.
// If Element Classic is taking too long to communicate (or crashes), unblock the user after a few seconds.
flow {
emit(NavigationEvent.Idle)
delay(5_000)
emit(NavigationEvent.NavigateToOnBoarding)
}
}
ElementClassicConnectionState.ElementClassicNotFound,
ElementClassicConnectionState.ElementClassicReadyNoSession,
is ElementClassicConnectionState.Error -> {
flowOf(NavigationEvent.NavigateToOnBoarding)
}
is ElementClassicConnectionState.ElementClassicReady -> {
val existingSessions = sessionStore.sessionsFlow().toUserListFlow().first()
if (elementClassicConnectionState.elementClassicSession.userId.value in existingSessions) {
flowOf(NavigationEvent.NavigateToOnBoarding)
} else {
// 2 cases when this can be run:
// First time this screen will be displayed
// Missing key backup screen was displayed, but the data has changed (user set up the key backup on Classic),
// and the app is resuming.
flowOf(NavigationEvent.NavigateToLoginWithClassic(elementClassicConnectionState.elementClassicSession.userId))
}
}
}
}
}
}
}

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ -38,16 +39,6 @@ class ClassicFlowNodeHelperTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `initial state`() = runTest {
createHelper()
.navigationEventFlow()
.test {
val initialState = awaitItem()
assertThat(initialState).isEqualTo(NavigationEvent.Idle)
}
}
@Test
fun `after a few seconds in Idle, NavigateToOnBoarding is emitted`() = runTest {
createHelper()
@ -57,6 +48,8 @@ class ClassicFlowNodeHelperTest {
assertThat(initialState).isEqualTo(NavigationEvent.Idle)
val finalState = awaitItem()
assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding)
advanceTimeBy(10_000)
expectNoEvents()
}
}
@ -82,6 +75,8 @@ class ClassicFlowNodeHelperTest {
)
val finalState = awaitItem()
assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding)
advanceTimeBy(10_000)
expectNoEvents()
}
}
@ -100,6 +95,8 @@ class ClassicFlowNodeHelperTest {
)
val finalState = awaitItem()
assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding)
advanceTimeBy(10_000)
expectNoEvents()
}
}
@ -118,6 +115,8 @@ class ClassicFlowNodeHelperTest {
)
val finalState = awaitItem()
assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding)
advanceTimeBy(10_000)
expectNoEvents()
}
}
@ -136,6 +135,8 @@ class ClassicFlowNodeHelperTest {
)
val finalState = awaitItem()
assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding)
advanceTimeBy(10_000)
expectNoEvents()
}
}
@ -154,6 +155,8 @@ class ClassicFlowNodeHelperTest {
)
val finalState = awaitItem()
assertThat(finalState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID))
advanceTimeBy(10_000)
expectNoEvents()
}
}
@ -178,6 +181,7 @@ class ClassicFlowNodeHelperTest {
avatar = createBitmap(1, 1)
)
)
advanceTimeBy(10_000)
expectNoEvents()
}
}
@ -211,6 +215,8 @@ class ClassicFlowNodeHelperTest {
)
val finalState = awaitItem()
assertThat(finalState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID))
advanceTimeBy(10_000)
expectNoEvents()
}
}
@ -236,6 +242,8 @@ class ClassicFlowNodeHelperTest {
)
val finalState = awaitItem()
assertThat(finalState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID))
advanceTimeBy(10_000)
expectNoEvents()
}
}
@ -264,6 +272,7 @@ class ClassicFlowNodeHelperTest {
sessionId = A_USER_ID.value,
)
)
advanceTimeBy(10_000)
expectNoEvents()
}
}