diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt index 11e6c65415..0850cbc4e9 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt @@ -87,20 +87,22 @@ class RoomSelectPresenter( val coroutineScope = rememberCoroutineScope() // Create one data source per session. When the selected session changes, the previous // data source is removed from the composition and its coroutine scope is cancelled. - val dataSource = key(selectedSessionId) { + val (dataSource, hasRoomListError) = key(selectedSessionId) { val dataSourceCoroutineScope = rememberCoroutineScope() - // The sessions are restored at startup, so the client should already be in memory - val roomListService by produceState(matrixClientProvider.getOrNull(selectedSessionId)?.roomListService) { + // The sessions are restored at startup, so the client should already be in memory. + // A null value means that the client is being restored. + val roomListServiceResult by produceState(matrixClientProvider.getOrNull(selectedSessionId)?.roomListService?.let { Result.success(it) }) { if (value == null) { value = matrixClientProvider.getOrRestore(selectedSessionId) .onFailure { Timber.e(it, "Failed to get the client for $selectedSessionId") } - .getOrNull() - ?.roomListService + .map { it.roomListService } } } - remember(roomListService) { + val roomListService = roomListServiceResult?.getOrNull() + val dataSource = remember(roomListService) { roomListService?.let { dataSourceFactory.create(dataSourceCoroutineScope, it) } } + dataSource to (roomListServiceResult?.isFailure == true) } val searchQuery = queryState.text.toString() @@ -156,6 +158,7 @@ class RoomSelectPresenter( isSearchActive = isSearchActive, selectedRooms = selectedRooms, selectedAccount = selectedAccount, + hasRoomListError = hasRoomListError, otherAccounts = otherAccounts, isAccountListExpanded = isAccountListExpanded, eventSink = ::handleEvent, diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt index 198ce2655d..7af7b28ddf 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt @@ -23,6 +23,10 @@ data class RoomSelectState( val isSearchActive: Boolean, val selectedRooms: ImmutableList, val selectedAccount: MatrixUser, + /** + * True if the rooms of the selected account cannot be loaded. + */ + val hasRoomListError: Boolean, val otherAccounts: ImmutableList, val isAccountListExpanded: Boolean, val eventSink: (RoomSelectEvent) -> Unit, diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStatePreviewParam.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStatePreviewParam.kt index d4e5ba011e..0c46c652ef 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStatePreviewParam.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStatePreviewParam.kt @@ -62,6 +62,12 @@ open class RoomSelectStatePreviewParam : PreviewParameterProvider = persistentListOf(), selectedAccount: MatrixUser = MatrixUser(UserId("@alice:example.org")), + hasRoomListError: Boolean = false, otherAccounts: ImmutableList = persistentListOf(), isAccountListExpanded: Boolean = false, eventSink: (RoomSelectEvent) -> Unit = {}, @@ -84,6 +91,7 @@ internal fun aRoomSelectState( isSearchActive = isSearchActive, selectedRooms = selectedRooms, selectedAccount = selectedAccount, + hasRoomListError = hasRoomListError, otherAccounts = otherAccounts, isAccountListExpanded = isAccountListExpanded, eventSink = eventSink, diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt index 224802c823..5865d4416a 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt @@ -54,6 +54,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.TopAppBarScrollBehaviorLayout import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarRow @@ -240,7 +242,9 @@ fun RoomSelectView( // showVerticalSpace only if there is no other accounts showVerticalSpace = !state.showAccountSwitch, ) - if (state.resultState is SearchBarResultState.Results) { + if (state.hasRoomListError) { + RoomListError() + } else if (state.resultState is SearchBarResultState.Results) { LazyColumn( state = lazyListState, contentPadding = lazyColumnContentPadding, @@ -270,6 +274,18 @@ fun RoomSelectView( } } +@Composable +private fun RoomListError() { + IconTitleSubtitleMolecule( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 32.dp), + title = stringResource(CommonStrings.common_something_went_wrong), + subTitle = stringResource(R.string.screen_room_select_error_cannot_load_rooms), + iconStyle = BigIcon.Style.AlertSolid, + ) +} + /** * Similar to the MultiAccountSection of the PreferencesRootView. * Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=5414-4759 diff --git a/libraries/roomselect/impl/src/main/res/values/localazy.xml b/libraries/roomselect/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..377391b835 --- /dev/null +++ b/libraries/roomselect/impl/src/main/res/values/localazy.xml @@ -0,0 +1,4 @@ + + + "The room list associated with this account cannot be loaded." + diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt index 3eeb3397d8..983d676dec 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -63,6 +64,45 @@ class RoomSelectPresenterTest { assertThat(initialState.otherAccounts).isEmpty() assertThat(initialState.isAccountListExpanded).isFalse() assertThat(initialState.showAccountSwitch).isFalse() + assertThat(initialState.hasRoomListError).isFalse() + } + } + + @Test + fun `present - failure to get the client shows an error`() = runTest { + val presenter = createRoomSelectPresenter( + matrixClientProvider = FakeMatrixClientProvider { Result.failure(AN_EXCEPTION) }, + ) + presenter.test { + val state = consumeItemsUntilPredicate { it.hasRoomListError }.last() + assertThat(state.hasRoomListError).isTrue() + assertThat(state.resultState).isInstanceOf(SearchBarResultState.Initial::class.java) + } + } + + @Test + fun `present - select another account whose client cannot be restored shows an error`() = runTest { + val presenter = createRoomSelectPresenter( + mode = RoomSelectMode.Share, + sessionStore = aSessionStoreWithTwoAccounts(), + matrixClientProvider = FakeMatrixClientProvider { sessionId -> + if (sessionId == A_SESSION_ID) { + Result.success(FakeMatrixClient(sessionId = sessionId)) + } else { + Result.failure(AN_EXCEPTION) + } + }, + ) + presenter.test { + val state = awaitLastSequentialItem() + assertThat(state.hasRoomListError).isFalse() + state.eventSink(RoomSelectEvent.SelectAccount(A_SESSION_ID_2)) + val errorState = consumeItemsUntilPredicate { it.hasRoomListError }.last() + assertThat(errorState.selectedAccount.userId).isEqualTo(A_SESSION_ID_2) + // Going back to the first account clears the error + errorState.eventSink(RoomSelectEvent.SelectAccount(A_SESSION_ID)) + val finalState = consumeItemsUntilPredicate { it.selectedAccount.userId == A_SESSION_ID && !it.hasRoomListError }.last() + assertThat(finalState.hasRoomListError).isFalse() } } diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectViewTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectViewTest.kt index 4abdddd8a8..543d20b521 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectViewTest.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectViewTest.kt @@ -122,6 +122,19 @@ class RoomSelectViewTest : RobolectricTest() { ) onNodeWithText(activity!!.getString(CommonStrings.common_switch_account)).assertDoesNotExist() } + + @Test + fun `an error is displayed instead of the rooms if they cannot be loaded`() = runAndroidComposeUiTest { + setRoomSelectView( + aRoomSelectState( + resultState = SearchBarResultState.Results(aRoomSelectRoomList()), + hasRoomListError = true, + ), + ) + onNodeWithText(activity!!.getString(CommonStrings.common_something_went_wrong)).assertIsDisplayed() + onNodeWithText(activity!!.getString(R.string.screen_room_select_error_cannot_load_rooms)).assertIsDisplayed() + onNodeWithText("Room with alias").assertDoesNotExist() + } } private fun AndroidComposeUiTest.setRoomSelectView( diff --git a/tools/localazy/config.json b/tools/localazy/config.json index e7f5f1e036..a46ae3adc1 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -14,6 +14,12 @@ "screen_bug_report_.*" ] }, + { + "name" : ":libraries:roomselect:impl", + "includeRegex" : [ + "screen\\.room_select\\..*" + ] + }, { "name" : ":features:rageshake:api", "includeRegex" : [