Merge pull request #7567 from hayaksi1/fix/4099-server-unreachable

Say when the server cannot be reached instead of showing nothing
This commit is contained in:
Benoit Marty
2026-08-26 16:56:40 +02:00
committed by GitHub
19 changed files with 101 additions and 34 deletions
@@ -105,9 +105,18 @@ class LoggedInPresenter(
} }
val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState() val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
val isOnline by syncService.isOnline.collectAsState() val isOnline by syncService.isOnline.collectAsState()
val showSyncSpinner by remember { val hasNetwork by networkMonitor.connectivity.collectAsState()
val syncIndicatorState by remember {
derivedStateOf { derivedStateOf {
isOnline && syncIndicator == RoomListService.SyncIndicator.Show when {
isOnline -> if (syncIndicator == RoomListService.SyncIndicator.Show) {
SyncIndicatorState.Syncing
} else {
SyncIndicatorState.Hidden
}
hasNetwork == NetworkStatus.Connected -> SyncIndicatorState.ServerUnreachable
else -> SyncIndicatorState.Hidden
}
} }
} }
var forceNativeSlidingSyncMigration by remember { mutableStateOf(false) } var forceNativeSlidingSyncMigration by remember { mutableStateOf(false) }
@@ -181,7 +190,7 @@ class LoggedInPresenter(
} }
return LoggedInState( return LoggedInState(
showSyncSpinner = showSyncSpinner, syncIndicatorState = syncIndicatorState,
pusherRegistrationState = pusherRegistrationState.value, pusherRegistrationState = pusherRegistrationState.value,
ignoreRegistrationError = ignoreRegistrationError, ignoreRegistrationError = ignoreRegistrationError,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
@@ -11,8 +11,14 @@ package io.element.android.appnav.loggedin
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
enum class SyncIndicatorState {
Hidden,
Syncing,
ServerUnreachable,
}
data class LoggedInState( data class LoggedInState(
val showSyncSpinner: Boolean, val syncIndicatorState: SyncIndicatorState,
val pusherRegistrationState: AsyncData<Unit>, val pusherRegistrationState: AsyncData<Unit>,
val ignoreRegistrationError: Boolean, val ignoreRegistrationError: Boolean,
val forceNativeSlidingSyncMigration: Boolean, val forceNativeSlidingSyncMigration: Boolean,
@@ -17,7 +17,8 @@ open class LoggedInStatePreviewParam : PreviewParameterProvider<LoggedInState> {
override val values: Sequence<LoggedInState> override val values: Sequence<LoggedInState>
get() = sequenceOf( get() = sequenceOf(
aLoggedInState(), aLoggedInState(),
aLoggedInState(showSyncSpinner = true), aLoggedInState(syncIndicatorState = SyncIndicatorState.Syncing),
aLoggedInState(syncIndicatorState = SyncIndicatorState.ServerUnreachable),
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())), aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
aLoggedInState(forceNativeSlidingSyncMigration = true), aLoggedInState(forceNativeSlidingSyncMigration = true),
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale), aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale),
@@ -26,13 +27,13 @@ open class LoggedInStatePreviewParam : PreviewParameterProvider<LoggedInState> {
} }
fun aLoggedInState( fun aLoggedInState(
showSyncSpinner: Boolean = false, syncIndicatorState: SyncIndicatorState = SyncIndicatorState.Hidden,
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized, pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
forceNativeSlidingSyncMigration: Boolean = false, forceNativeSlidingSyncMigration: Boolean = false,
appName: String = "Element X", appName: String = "Element X",
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None, localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
) = LoggedInState( ) = LoggedInState(
showSyncSpinner = showSyncSpinner, syncIndicatorState = syncIndicatorState,
pusherRegistrationState = pusherRegistrationState, pusherRegistrationState = pusherRegistrationState,
ignoreRegistrationError = false, ignoreRegistrationError = false,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
@@ -47,7 +47,7 @@ fun LoggedInView(
) { ) {
SyncStateView( SyncStateView(
modifier = Modifier.align(Alignment.TopCenter), modifier = Modifier.align(Alignment.TopCenter),
isVisible = state.showSyncSpinner, state = state.syncIndicatorState,
) )
} }
when (state.pusherRegistrationState) { when (state.pusherRegistrationState) {
@@ -25,18 +25,23 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun SyncStateView( fun SyncStateView(
isVisible: Boolean, state: SyncIndicatorState,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = isVisible, visible = state != SyncIndicatorState.Hidden,
modifier = modifier, modifier = modifier,
enter = fadeIn(spring(stiffness = 500F)), enter = fadeIn(spring(stiffness = 500F)),
exit = fadeOut(spring(stiffness = 500F)), exit = fadeOut(spring(stiffness = 500F)),
) { ) {
AsyncIndicator.Loading( when (state) {
text = stringResource(id = CommonStrings.common_syncing), SyncIndicatorState.ServerUnreachable -> AsyncIndicator.Failure(
) text = stringResource(id = CommonStrings.common_server_unreachable),
)
else -> AsyncIndicator.Loading(
text = stringResource(id = CommonStrings.common_syncing),
)
}
} }
} }
@@ -46,7 +51,18 @@ internal fun SyncStateViewPreview() = ElementPreview {
// Add a box to see the shadow // Add a box to see the shadow
Box(modifier = Modifier.padding(24.dp)) { Box(modifier = Modifier.padding(24.dp)) {
SyncStateView( SyncStateView(
isVisible = true state = SyncIndicatorState.Syncing
)
}
}
@PreviewsDayNight
@Composable
internal fun SyncStateViewServerUnreachablePreview() = ElementPreview {
// Add a box to see the shadow
Box(modifier = Modifier.padding(24.dp)) {
SyncStateView(
state = SyncIndicatorState.ServerUnreachable
) )
} }
} }
@@ -67,7 +67,7 @@ class LoggedInPresenterTest {
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
createLoggedInPresenter().test { createLoggedInPresenter().test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.showSyncSpinner).isFalse() assertThat(initialState.syncIndicatorState).isEqualTo(SyncIndicatorState.Hidden)
assertThat(initialState.pusherRegistrationState.isUninitialized()).isTrue() assertThat(initialState.pusherRegistrationState.isUninitialized()).isTrue()
assertThat(initialState.ignoreRegistrationError).isFalse() assertThat(initialState.ignoreRegistrationError).isFalse()
} }
@@ -97,11 +97,33 @@ class LoggedInPresenterTest {
matrixClient = FakeMatrixClient(roomListService = roomListService), matrixClient = FakeMatrixClient(roomListService = roomListService),
).test { ).test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.showSyncSpinner).isFalse() assertThat(initialState.syncIndicatorState).isEqualTo(SyncIndicatorState.Hidden)
roomListService.postSyncIndicator(RoomListService.SyncIndicator.Show) roomListService.postSyncIndicator(RoomListService.SyncIndicator.Show)
consumeItemsUntilPredicate { it.showSyncSpinner } consumeItemsUntilPredicate { it.syncIndicatorState == SyncIndicatorState.Syncing }
roomListService.postSyncIndicator(RoomListService.SyncIndicator.Hide) roomListService.postSyncIndicator(RoomListService.SyncIndicator.Hide)
consumeItemsUntilPredicate { !it.showSyncSpinner } consumeItemsUntilPredicate { it.syncIndicatorState != SyncIndicatorState.Syncing }
}
}
@Test
fun `present - say the server is unreachable when the device has network but the sync is offline`() = runTest {
createLoggedInPresenter(
syncState = SyncState.Offline,
networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected),
).test {
consumeItemsUntilPredicate { it.syncIndicatorState == SyncIndicatorState.ServerUnreachable }
cancelAndConsumeRemainingEvents()
}
}
@Test
fun `present - say nothing when the device itself has no network`() = runTest {
createLoggedInPresenter(
syncState = SyncState.Offline,
networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected),
).test {
consumeItemsUntilPredicate { it.syncIndicatorState == SyncIndicatorState.Hidden }
cancelAndConsumeRemainingEvents()
} }
} }
@@ -133,6 +133,7 @@ class KonsistPreviewTest {
"SecureBackupSetupViewChangePreview", "SecureBackupSetupViewChangePreview",
"SelectedUserCannotRemovePreview", "SelectedUserCannotRemovePreview",
"SpaceMembersViewNoHeroesPreview", "SpaceMembersViewNoHeroesPreview",
"SyncStateViewServerUnreachablePreview",
"TextComposerAddCaptionPreview", "TextComposerAddCaptionPreview",
"TextComposerCaptionPreview", "TextComposerCaptionPreview",
"TextComposerEditCaptionPreview", "TextComposerEditCaptionPreview",