From 463b9e06427f2d91c1adc63a6d388416f94ef969 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 23 May 2023 10:23:24 +0100 Subject: [PATCH] Invite users to existing rooms (#441) Invite users to existing rooms Scope: - Allow inviting from the room detail screen and the member list - Invite option is only shown if the user has the correct power level - Search flow the same as creating a new room, allowing multi-select - Existing room members/invitees are disabled with a custom caption - Sending is asynchronous, an error dialog will appear wherever the user is if necessary Closes #245 --- appnav/build.gradle.kts | 1 + .../android/appnav/root/RootPresenter.kt | 6 + .../element/android/appnav/root/RootState.kt | 2 + .../android/appnav/root/RootStateProvider.kt | 6 + .../element/android/appnav/root/RootView.kt | 4 + .../android/appnav/RootPresenterTest.kt | 31 +- changelog.d/245.feature | 1 + .../impl/addpeople/AddPeopleView.kt | 20 +- .../impl/components/SearchUserBar.kt | 2 + .../impl/components/UserListView.kt | 2 + features/roomdetails/impl/build.gradle.kts | 2 + .../roomdetails/impl/RoomDetailsFlowNode.kt | 15 + .../roomdetails/impl/RoomDetailsNode.kt | 6 + .../roomdetails/impl/RoomDetailsPresenter.kt | 13 +- .../roomdetails/impl/RoomDetailsState.kt | 1 + .../impl/RoomDetailsStateProvider.kt | 2 + .../roomdetails/impl/RoomDetailsView.kt | 21 +- .../impl/invite/RoomInviteMembersEvents.kt | 25 ++ .../impl/invite/RoomInviteMembersNode.kt | 78 +++++ .../impl/invite/RoomInviteMembersPresenter.kt | 152 ++++++++ .../impl/invite/RoomInviteMembersState.kt | 38 ++ .../invite/RoomInviteMembersStateProvider.kt | 52 +++ .../impl/invite/RoomInviteMembersView.kt | 218 ++++++++++++ .../impl/members/RoomMemberListNode.kt | 9 +- .../impl/members/RoomMemberListPresenter.kt | 18 + .../impl/members/RoomMemberListState.kt | 1 + .../members/RoomMemberListStateProvider.kt | 2 + .../impl/members/RoomMemberListView.kt | 26 +- .../roomdetails/RoomDetailsPresenterTests.kt | 61 +++- .../invite/RoomInviteMembersPresenterTest.kt | 331 ++++++++++++++++++ .../members/RoomMemberListPresenterTests.kt | 52 ++- .../theme/components/SearchBar.kt | 15 +- .../libraries/matrix/api/room/MatrixRoom.kt | 4 + .../matrix/impl/room/RustMatrixRoom.kt | 13 + .../impl/timeline/RustMatrixTimeline.kt | 1 + .../matrix/test/room/FakeMatrixRoom.kt | 22 ++ .../src/main/res/values/localazy.xml | 5 +- .../kotlin/extension/DependencyHandleScope.kt | 1 + services/apperror/api/build.gradle.kts | 27 ++ .../services/apperror/api/AppErrorState.kt | 29 ++ .../apperror/api/AppErrorStateProvider.kt | 23 ++ .../apperror/api/AppErrorStateService.kt | 27 ++ services/apperror/impl/build.gradle.kts | 51 +++ .../services/apperror/impl/AppErrorView.kt | 66 ++++ .../impl/DefaultAppErrorStateService.kt | 44 +++ .../impl/DefaultAppErrorStateServiceTest.kt | 72 ++++ ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...rsDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...rsDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...rsDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...rsDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...rsDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...rsDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...sLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...sLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...sLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...sLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...sLightPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...sLightPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...stDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...stDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...stDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...stDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...stDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 4 +- ...stDarkPreview_0_null_7,NEXUS_5,1.0,en].png | 3 + ...tLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...tLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...tLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...tLightPreview_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...tLightPreview_0_null_6,NEXUS_5,1.0,en].png | 4 +- ...tLightPreview_0_null_7,NEXUS_5,1.0,en].png | 3 + ...arkPreview--1_1_null_0,NEXUS_5,1.0,en].png | 4 +- ...arkPreview--1_1_null_1,NEXUS_5,1.0,en].png | 4 +- ...arkPreview--1_1_null_2,NEXUS_5,1.0,en].png | 4 +- ...arkPreview--1_1_null_3,NEXUS_5,1.0,en].png | 4 +- ...arkPreview--1_1_null_4,NEXUS_5,1.0,en].png | 4 +- ...arkPreview--1_1_null_7,NEXUS_5,1.0,en].png | 3 + ...ghtPreview--0_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...ghtPreview--0_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...ghtPreview--0_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...ghtPreview--0_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...ghtPreview--0_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...ghtPreview--0_0_null_7,NEXUS_5,1.0,en].png | 3 + ...eryNoBackButton_0_null,NEXUS_5,1.0,en].png | 3 + 85 files changed, 1668 insertions(+), 69 deletions(-) create mode 100644 changelog.d/245.feature create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersEvents.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt create mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt create mode 100644 services/apperror/api/build.gradle.kts create mode 100644 services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt create mode 100644 services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateProvider.kt create mode 100644 services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateService.kt create mode 100644 services/apperror/impl/build.gradle.kts create mode 100644 services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt create mode 100644 services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateService.kt create mode 100644 services/apperror/impl/src/test/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateServiceTest.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithQueryNoBackButton_0_null,NEXUS_5,1.0,en].png diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 9ba856aa4c..5b1c4b1ced 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(projects.tests.uitests) implementation(libs.coil) + implementation(projects.services.apperror.impl) implementation(projects.services.appnavstate.api) testImplementation(libs.test.junit) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt index 9ac4d34b98..cffc4cf35c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt @@ -17,24 +17,30 @@ package io.element.android.appnav.root import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter import io.element.android.features.rageshake.api.detection.RageshakeDetectionPresenter import io.element.android.libraries.architecture.Presenter +import io.element.android.services.apperror.api.AppErrorStateService import javax.inject.Inject class RootPresenter @Inject constructor( private val crashDetectionPresenter: CrashDetectionPresenter, private val rageshakeDetectionPresenter: RageshakeDetectionPresenter, + private val appErrorStateService: AppErrorStateService, ) : Presenter { @Composable override fun present(): RootState { val rageshakeDetectionState = rageshakeDetectionPresenter.present() val crashDetectionState = crashDetectionPresenter.present() + val appErrorState by appErrorStateService.appErrorStateFlow.collectAsState() return RootState( rageshakeDetectionState = rageshakeDetectionState, crashDetectionState = crashDetectionState, + errorState = appErrorState, ) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt index 8389e1f144..704adb5df3 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt @@ -19,9 +19,11 @@ package io.element.android.appnav.root import androidx.compose.runtime.Immutable import io.element.android.features.rageshake.api.crash.CrashDetectionState import io.element.android.features.rageshake.api.detection.RageshakeDetectionState +import io.element.android.services.apperror.api.AppErrorState @Immutable data class RootState( val rageshakeDetectionState: RageshakeDetectionState, val crashDetectionState: CrashDetectionState, + val errorState: AppErrorState, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt index 645fbccd0d..c8b5413e60 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt @@ -19,6 +19,8 @@ package io.element.android.appnav.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.rageshake.api.crash.aCrashDetectionState import io.element.android.features.rageshake.api.detection.aRageshakeDetectionState +import io.element.android.services.apperror.api.AppErrorState +import io.element.android.services.apperror.api.aAppErrorState open class RootStateProvider : PreviewParameterProvider { override val values: Sequence @@ -30,6 +32,9 @@ open class RootStateProvider : PreviewParameterProvider { aRootState().copy( rageshakeDetectionState = aRageshakeDetectionState().copy(showDialog = true), crashDetectionState = aCrashDetectionState().copy(crashDetected = false), + ), + aRootState().copy( + errorState = aAppErrorState(), ) ) } @@ -37,4 +42,5 @@ open class RootStateProvider : PreviewParameterProvider { fun aRootState() = RootState( rageshakeDetectionState = aRageshakeDetectionState(), crashDetectionState = aCrashDetectionState(), + errorState = AppErrorState.NoError, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt index fc25ee65b1..a52ee59261 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt @@ -31,6 +31,7 @@ import io.element.android.features.rageshake.api.detection.RageshakeDetectionVie import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.services.apperror.impl.AppErrorView @Composable fun RootView( @@ -60,6 +61,9 @@ fun RootView( state = state.crashDetectionState, onOpenBugReport = ::onOpenBugReport, ) + AppErrorView( + state = state.errorState, + ) } } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt index 7c45620afd..0efa9e7f3b 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt @@ -28,6 +28,9 @@ import io.element.android.features.rageshake.test.crash.FakeCrashDataStore import io.element.android.features.rageshake.test.rageshake.FakeRageShake import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder +import io.element.android.services.apperror.api.AppErrorState +import io.element.android.services.apperror.api.AppErrorStateService +import io.element.android.services.apperror.impl.DefaultAppErrorStateService import kotlinx.coroutines.test.runTest import org.junit.Test @@ -44,7 +47,32 @@ class RootPresenterTest { } } - private fun createPresenter(): RootPresenter { + @Test + fun `present - passes app error state`() = runTest { + val presenter = createPresenter( + appErrorService = DefaultAppErrorStateService().apply { + showError("Bad news", "Something bad happened") + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + + val initialState = awaitItem() + assertThat(initialState.errorState).isInstanceOf(AppErrorState.Error::class.java) + val initialErrorState = initialState.errorState as AppErrorState.Error + assertThat(initialErrorState.title).isEqualTo("Bad news") + assertThat(initialErrorState.body).isEqualTo("Something bad happened") + + initialErrorState.dismiss() + assertThat(awaitItem().errorState).isInstanceOf(AppErrorState.NoError::class.java) + } + } + + private fun createPresenter( + appErrorService: AppErrorStateService = DefaultAppErrorStateService() + ): RootPresenter { val crashDataStore = FakeCrashDataStore() val rageshakeDataStore = FakeRageshakeDataStore() val rageshake = FakeRageShake() @@ -63,6 +91,7 @@ class RootPresenterTest { return RootPresenter( crashDetectionPresenter = crashDetectionPresenter, rageshakeDetectionPresenter = rageshakeDetectionPresenter, + appErrorStateService = appErrorService, ) } } diff --git a/changelog.d/245.feature b/changelog.d/245.feature new file mode 100644 index 0000000000..204107717a --- /dev/null +++ b/changelog.d/245.feature @@ -0,0 +1 @@ +[Create and join rooms] Add ability to invite users to existing rooms diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index 8bb82d3d2d..c64c1efc75 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.createroom.impl.R import io.element.android.features.createroom.impl.components.UserListView +import io.element.android.features.createroom.impl.userlist.UserListEvents import io.element.android.features.createroom.impl.userlist.UserListState import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -54,13 +55,17 @@ fun AddPeopleView( Scaffold( modifier = modifier, topBar = { - if (!state.isSearchActive) { - AddPeopleViewTopBar( - hasSelectedUsers = state.selectedUsers.isNotEmpty(), - onBackPressed = onBackPressed, - onNextPressed = onNextPressed, - ) - } + AddPeopleViewTopBar( + hasSelectedUsers = state.selectedUsers.isNotEmpty(), + onBackPressed = { + if (state.isSearchActive) { + state.eventSink(UserListEvents.OnSearchActiveChanged(false)) + } else { + onBackPressed() + } + }, + onNextPressed = onNextPressed, + ) } ) { padding -> Column( @@ -73,6 +78,7 @@ fun AddPeopleView( modifier = Modifier .fillMaxWidth(), state = state, + showBackButton = false, ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt index 33d308941f..be87e1cf51 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt @@ -39,6 +39,7 @@ fun SearchUserBar( active: Boolean, isMultiSelectionEnabled: Boolean, modifier: Modifier = Modifier, + showBackButton: Boolean = true, placeHolderTitle: String = stringResource(R.string.common_search_for_someone), onActiveChanged: (Boolean) -> Unit = {}, onTextChanged: (String) -> Unit = {}, @@ -52,6 +53,7 @@ fun SearchUserBar( onActiveChange = onActiveChanged, modifier = modifier, placeHolderTitle = placeHolderTitle, + showBackButton = showBackButton, contentPrefix = { if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) { SelectedUsersList( diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt index 299dc59abf..7d543261fc 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt @@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.ui.components.SelectedUsersList fun UserListView( state: UserListState, modifier: Modifier = Modifier, + showBackButton: Boolean = true, onUserSelected: (MatrixUser) -> Unit = {}, onUserDeselected: (MatrixUser) -> Unit = {}, ) { @@ -49,6 +50,7 @@ fun UserListView( selectedUsers = state.selectedUsers, active = state.isSearchActive, isMultiSelectionEnabled = state.isMultiSelectionEnabled, + showBackButton = showBackButton, onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) }, onTextChanged = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) }, onUserSelected = { diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 05181c3f67..0470fc2e65 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(projects.libraries.androidutils) api(projects.features.roomdetails.api) api(projects.libraries.usersearch.api) + api(projects.services.apperror.api) implementation(libs.coil.compose) testImplementation(libs.test.junit) @@ -51,6 +52,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.usersearch.test) testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 8c7d9cd375..97fb3311cb 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -28,6 +28,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode import io.element.android.features.roomdetails.impl.members.RoomMemberListNode import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode import io.element.android.libraries.architecture.BackstackNode @@ -57,6 +58,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize object RoomMemberList : NavTarget + @Parcelize + object InviteMembers : NavTarget + @Parcelize data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget } @@ -68,6 +72,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( override fun openRoomMemberList() { backstack.push(NavTarget.RoomMemberList) } + + override fun openInviteMembers() { + backstack.push(NavTarget.InviteMembers) + } } createNode(buildContext, listOf(roomDetailsCallback)) } @@ -76,9 +84,16 @@ class RoomDetailsFlowNode @AssistedInject constructor( override fun openRoomMemberDetails(roomMemberId: UserId) { backstack.push(NavTarget.RoomMemberDetails(roomMemberId)) } + + override fun openInviteMembers() { + backstack.push(NavTarget.InviteMembers) + } } createNode(buildContext, listOf(roomMemberListCallback)) } + NavTarget.InviteMembers -> { + createNode(buildContext) + } is NavTarget.RoomMemberDetails -> { createNode(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMemberId))) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 37110e5192..8fe4f774d7 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -45,6 +45,7 @@ class RoomDetailsNode @AssistedInject constructor( interface Callback : Plugin { fun openRoomMemberList() + fun openInviteMembers() } private val callbacks = plugins() @@ -53,6 +54,10 @@ class RoomDetailsNode @AssistedInject constructor( callbacks.forEach { it.openRoomMemberList() } } + private fun invitePeople() { + callbacks.forEach { it.openInviteMembers() } + } + private fun onShareRoom(context: Context) { val alias = room.alias ?: room.alternativeAliases.firstOrNull() val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) } @@ -105,6 +110,7 @@ class RoomDetailsNode @AssistedInject constructor( onShareRoom = ::onShareRoom, onShareMember = ::onShareMember, openRoomMemberList = ::openRoomMemberList, + invitePeople = ::invitePeople, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 81b2e1e383..01562841ea 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -62,6 +62,7 @@ class RoomDetailsPresenter @Inject constructor( val membersState by room.membersStateFlow.collectAsState() val memberCount by getMemberCount(membersState) + val canInvite by getCanInvite(membersState) val dmMember by room.getDirectRoomMember(membersState) val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) val roomType = getRoomType(dmMember) @@ -76,7 +77,7 @@ class RoomDetailsPresenter @Inject constructor( error = error, ) } - is RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning.value = null + RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning.value = null RoomDetailsEvent.ClearError -> error.value = null } } @@ -91,6 +92,7 @@ class RoomDetailsPresenter @Inject constructor( roomTopic = room.topic, memberCount = memberCount, isEncrypted = room.isEncrypted, + canInvite = canInvite, displayLeaveRoomWarning = leaveRoomWarning.value, error = error.value, roomType = roomType.value, @@ -117,6 +119,15 @@ class RoomDetailsPresenter @Inject constructor( } } + @Composable + private fun getCanInvite(membersState: MatrixRoomMembersState): State { + val canInvite = remember(membersState) { mutableStateOf(false) } + LaunchedEffect(membersState) { + canInvite.value = room.canInvite().getOrElse { false } + } + return canInvite + } + @Composable private fun getMemberCount(membersState: MatrixRoomMembersState): State> { return remember(membersState) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index 173ba66ed0..90fa48c575 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -32,6 +32,7 @@ data class RoomDetailsState( val error: RoomDetailsError?, val roomType: RoomDetailsType, val roomMemberDetailsState: RoomMemberDetailsState?, + val canInvite: Boolean, val eventSink: (RoomDetailsEvent) -> Unit ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index b58ae355dd..08da243487 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -33,6 +33,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider aRoomDetailsState().copy(memberCount = Async.Failure(Throwable())), aDmRoomDetailsState().copy(roomName = "Daniel"), aDmRoomDetailsState(isDmMemberIgnored = true).copy(roomName = "Daniel"), + aRoomDetailsState().copy(canInvite = true), // Add other state here ) } @@ -71,6 +72,7 @@ fun aRoomDetailsState() = RoomDetailsState( isEncrypted = true, displayLeaveRoomWarning = null, error = null, + canInvite = false, roomType = RoomDetailsType.Room, roomMemberDetailsState = null, eventSink = {} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index c09a80cf1b..293f290112 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -78,6 +78,7 @@ fun RoomDetailsView( onShareRoom: () -> Unit, onShareMember: (RoomMember) -> Unit, openRoomMemberList: () -> Unit, + invitePeople: () -> Unit, modifier: Modifier = Modifier, ) { @@ -127,7 +128,9 @@ fun RoomDetailsView( MembersSection( memberCount = memberCount, isLoading = state.memberCount.isLoading(), - openRoomMemberList = openRoomMemberList + showInvite = state.canInvite, + openRoomMemberList = openRoomMemberList, + invitePeople = invitePeople, ) } @@ -211,8 +214,10 @@ internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) { internal fun MembersSection( memberCount: Int?, isLoading: Boolean, + showInvite: Boolean, + invitePeople: () -> Unit, + openRoomMemberList: () -> Unit, modifier: Modifier = Modifier, - openRoomMemberList: () -> Unit ) { PreferenceCategory(modifier = modifier) { PreferenceText( @@ -222,10 +227,13 @@ internal fun MembersSection( onClick = openRoomMemberList, loadingCurrentValue = isLoading, ) - PreferenceText( - title = stringResource(R.string.screen_room_details_invite_people_title), - icon = Icons.Outlined.PersonAddAlt, - ) + if (showInvite) { + PreferenceText( + title = stringResource(R.string.screen_room_details_invite_people_title), + icon = Icons.Outlined.PersonAddAlt, + onClick = invitePeople, + ) + } } } @@ -291,5 +299,6 @@ private fun ContentToPreview(state: RoomDetailsState) { onShareRoom = {}, onShareMember = {}, openRoomMemberList = {}, + invitePeople = {}, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersEvents.kt new file mode 100644 index 0000000000..80be60fb8c --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersEvents.kt @@ -0,0 +1,25 @@ +/* + * 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.features.roomdetails.impl.invite + +import io.element.android.libraries.matrix.api.user.MatrixUser + +sealed interface RoomInviteMembersEvents { + data class ToggleUser(val user: MatrixUser) : RoomInviteMembersEvents + data class UpdateSearchQuery(val query: String) : RoomInviteMembersEvents + data class OnSearchActiveChanged(val active: Boolean) : RoomInviteMembersEvents +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt new file mode 100644 index 0000000000..22e91ce5a7 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt @@ -0,0 +1,78 @@ +/* + * 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.features.roomdetails.impl.invite + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.services.apperror.api.AppErrorStateService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import io.element.android.libraries.ui.strings.R as StringR + +@ContributesNode(RoomScope::class) +class RoomInviteMembersNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + coroutineDispatchers: CoroutineDispatchers, + private val room: MatrixRoom, + private val presenter: RoomInviteMembersPresenter, + private val appErrorStateService: AppErrorStateService, +) : Node(buildContext, plugins = plugins) { + + private val coroutineScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.io) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val context = LocalContext.current.applicationContext + + RoomInviteMembersView( + state = state, + modifier = modifier, + onBackPressed = { navigateUp() }, + onSendPressed = { users -> + navigateUp() + + coroutineScope.launch { + val anyInviteFailed = users + .map { room.inviteUserById(it.userId) } + .any { it.isFailure } + + if (anyInviteFailed) { + appErrorStateService.showError( + title = context.getString(StringR.string.common_unable_to_invite_title), + body = context.getString(StringR.string.common_unable_to_invite_message), + ) + } + + room.updateMembers() + } + } + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt new file mode 100644 index 0000000000..07b3bb1708 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt @@ -0,0 +1,152 @@ +/* + * 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.features.roomdetails.impl.invite + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.execute +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.usersearch.api.UserRepository +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class RoomInviteMembersPresenter @Inject constructor( + private val userRepository: UserRepository, + private val roomMemberListDataSource: RoomMemberListDataSource, + private val coroutineDispatchers: CoroutineDispatchers, +) : Presenter { + + @Composable + override fun present(): RoomInviteMembersState { + val roomMembers = remember { mutableStateOf>>(Async.Loading()) } + val selectedUsers = remember { mutableStateOf>(persistentListOf()) } + val searchResults = remember { mutableStateOf>>(SearchBarResultState.NotSearching()) } + var searchQuery by rememberSaveable { mutableStateOf("") } + var searchActive by rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(Unit) { + fetchMembers(roomMembers) + } + + LaunchedEffect(searchQuery, roomMembers) { + performSearch(searchResults, roomMembers, selectedUsers, searchQuery) + } + + return RoomInviteMembersState( + canInvite = selectedUsers.value.isNotEmpty(), + selectedUsers = selectedUsers.value, + searchQuery = searchQuery, + isSearchActive = searchActive, + searchResults = searchResults.value, + eventSink = { + when (it) { + is RoomInviteMembersEvents.OnSearchActiveChanged -> { + searchActive = it.active + searchQuery = "" + } + + is RoomInviteMembersEvents.UpdateSearchQuery -> { + searchQuery = it.query + } + + is RoomInviteMembersEvents.ToggleUser -> { + selectedUsers.toggleUser(it.user) + searchResults.toggleUser(it.user) + } + } + } + ) + } + + @JvmName("toggleUserInSelectedUsers") + private fun MutableState>.toggleUser(user: MatrixUser) { + value = if (value.contains(user)) { + value.filterNot { it == user } + } else { + (value + user) + }.toImmutableList() + } + + @JvmName("toggleUserInSearchResults") + private fun MutableState>>.toggleUser(user: MatrixUser) { + val existingResults = value + if (existingResults is SearchBarResultState.Results) { + value = SearchBarResultState.Results( + existingResults.results.map { iu -> + if (iu.matrixUser == user) { + iu.copy(isSelected = !iu.isSelected) + } else { + iu + } + }.toImmutableList() + ) + } + } + + private suspend fun performSearch( + searchResults: MutableState>>, + roomMembers: MutableState>>, + selectedUsers: MutableState>, + searchQuery: String, + ) = withContext(coroutineDispatchers.io) { + searchResults.value = SearchBarResultState.NotSearching() + + val joinedMembers = roomMembers.value.dataOrNull().orEmpty() + + userRepository.search(searchQuery).collect { + searchResults.value = when { + it.isEmpty() -> SearchBarResultState.NoResults() + else -> SearchBarResultState.Results(it.map { user -> + val existingMembership = joinedMembers.firstOrNull { j -> j.userId == user.userId }?.membership + val isJoined = existingMembership == RoomMembershipState.JOIN + val isInvited = existingMembership == RoomMembershipState.INVITE + InvitableUser( + matrixUser = user, + isSelected = selectedUsers.value.contains(user) || isJoined || isInvited, + isAlreadyJoined = isJoined, + isAlreadyInvited = isInvited, + ) + }.toImmutableList()) + } + } + } + + private suspend fun fetchMembers(roomMembers: MutableState>>) { + suspend { + withContext(coroutineDispatchers.io) { + roomMemberListDataSource.search("").toImmutableList() + } + }.execute(roomMembers) + } +} + diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt new file mode 100644 index 0000000000..b96cc30935 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt @@ -0,0 +1,38 @@ +/* + * 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.features.roomdetails.impl.invite + +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +data class RoomInviteMembersState( + val canInvite: Boolean = false, + val searchQuery: String = "", + val searchResults: SearchBarResultState> = SearchBarResultState.NotSearching(), + val selectedUsers: ImmutableList = persistentListOf(), + val isSearchActive: Boolean = false, + val eventSink: (RoomInviteMembersEvents) -> Unit = {}, +) + +data class InvitableUser( + val matrixUser: MatrixUser, + val isSelected: Boolean = false, + val isAlreadyJoined: Boolean = false, + val isAlreadyInvited: Boolean = false, +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt new file mode 100644 index 0000000000..137d47b660 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt @@ -0,0 +1,52 @@ +/* + * 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.features.roomdetails.impl.invite + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class RoomInviteMembersStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + RoomInviteMembersState(), + RoomInviteMembersState(canInvite = true, selectedUsers = aMatrixUserList().toImmutableList()), + RoomInviteMembersState(isSearchActive = true, searchQuery = "some query"), + RoomInviteMembersState(isSearchActive = true, searchQuery = "some query", selectedUsers = aMatrixUserList().toImmutableList()), + RoomInviteMembersState(isSearchActive = true, searchQuery = "some query", searchResults = SearchBarResultState.NoResults()), + RoomInviteMembersState( + isSearchActive = true, + canInvite = true, + searchQuery = "some query", + selectedUsers = persistentListOf( + aMatrixUser("@carol:server.org", "Carol") + ), + searchResults = SearchBarResultState.Results( + persistentListOf( + InvitableUser(aMatrixUser("@alice:server.org")), + InvitableUser(aMatrixUser("@bob:server.org", "Bob")), + InvitableUser(aMatrixUser("@carol:server.org", "Carol"), isSelected = true), + InvitableUser(aMatrixUser("@eve:server.org", "Eve"), isSelected = true, isAlreadyJoined = true), + InvitableUser(aMatrixUser("@justin:server.org", "Justin"), isSelected = true, isAlreadyInvited = true), + ) + ) + ), + ) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt new file mode 100644 index 0000000000..313ad25346 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt @@ -0,0 +1,218 @@ +/* + * 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.features.roomdetails.impl.invite + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.features.roomdetails.impl.R +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.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.SearchBar +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.CheckableUserRow +import io.element.android.libraries.matrix.ui.components.SelectedUsersList +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.matrix.ui.model.getBestName +import kotlinx.collections.immutable.ImmutableList +import io.element.android.libraries.ui.strings.R as StringR + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun RoomInviteMembersView( + state: RoomInviteMembersState, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, + onSendPressed: (List) -> Unit = {}, +) { + Scaffold( + topBar = { + RoomInviteMembersTopBar( + onBackPressed = { + if (state.isSearchActive) { + state.eventSink(RoomInviteMembersEvents.OnSearchActiveChanged(false)) + } else { + onBackPressed() + } + }, + onSendPressed = { onSendPressed(state.selectedUsers) }, + canSend = state.canInvite, + ) + } + ) { padding -> + Column( + modifier = modifier + .fillMaxWidth() + .padding(padding) + .consumeWindowInsets(padding), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + RoomInviteMembersSearchBar( + modifier = Modifier.fillMaxWidth(), + query = state.searchQuery, + selectedUsers = state.selectedUsers, + state = state.searchResults, + active = state.isSearchActive, + onActiveChanged = { state.eventSink(RoomInviteMembersEvents.OnSearchActiveChanged(it)) }, + onTextChanged = { state.eventSink(RoomInviteMembersEvents.UpdateSearchQuery(it)) }, + onUserToggled = { state.eventSink(RoomInviteMembersEvents.ToggleUser(it)) }, + ) + + if (!state.isSearchActive) { + SelectedUsersList( + modifier = Modifier.fillMaxWidth(), + selectedUsers = state.selectedUsers, + autoScroll = true, + onUserRemoved = { state.eventSink(RoomInviteMembersEvents.ToggleUser(it)) }, + contentPadding = PaddingValues(16.dp), + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomInviteMembersTopBar( + canSend: Boolean, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, + onSendPressed: () -> Unit = {}, +) { + CenterAlignedTopAppBar( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.screen_room_details_invite_people_title), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ) + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + actions = { + TextButton( + onClick = onSendPressed, + content = { + Text(stringResource(StringR.string.action_send)) + }, + enabled = canSend, + ) + } + ) +} + +@Composable +private fun RoomInviteMembersSearchBar( + query: String, + state: SearchBarResultState>, + selectedUsers: ImmutableList, + active: Boolean, + modifier: Modifier = Modifier, + placeHolderTitle: String = stringResource(io.element.android.libraries.ui.strings.R.string.common_search_for_someone), + onActiveChanged: (Boolean) -> Unit = {}, + onTextChanged: (String) -> Unit = {}, + onUserToggled: (MatrixUser) -> Unit = {}, +) { + SearchBar( + query = query, + onQueryChange = onTextChanged, + active = active, + onActiveChange = onActiveChanged, + modifier = modifier, + placeHolderTitle = placeHolderTitle, + contentPrefix = { + if (selectedUsers.isNotEmpty()) { + SelectedUsersList( + modifier = Modifier.fillMaxWidth(), + selectedUsers = selectedUsers, + autoScroll = true, + onUserRemoved = onUserToggled, + contentPadding = PaddingValues(16.dp), + ) + } + }, + showBackButton = false, + resultState = state, + resultHandler = { results -> + Text( + text = "Search results", + fontWeight = FontWeight.Medium, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 8.dp) + ) + + LazyColumn { + items(results) { invitableUser -> + CheckableUserRow( + checked = invitableUser.isSelected, + enabled = !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined, + avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.MEDIUM), + name = invitableUser.matrixUser.getBestName(), + subtext = when { + // If they're already invited or joined we show that information + invitableUser.isAlreadyJoined -> stringResource(R.string.screen_room_details_already_a_member) + invitableUser.isAlreadyInvited -> stringResource(R.string.screen_room_details_already_invited) + // Otherwise show the ID, unless that's already used for their name + invitableUser.matrixUser.displayName.isNullOrEmpty().not() -> invitableUser.matrixUser.userId.value + else -> null + }, + onCheckedChange = { onUserToggled(invitableUser.matrixUser) }, + modifier = Modifier.fillMaxWidth() + ) + } + } + }, + ) +} + +@Preview +@Composable +fun RoomInviteMembersLightPreview(@PreviewParameter(RoomInviteMembersStateProvider::class) state: RoomInviteMembersState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun RoomInviteMembersDarkPreview(@PreviewParameter(RoomInviteMembersStateProvider::class) state: RoomInviteMembersState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: RoomInviteMembersState) { + RoomInviteMembersView(state) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index 5b1e7a72e0..f37ea1229b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -27,7 +27,6 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.RoomMember @ContributesNode(RoomScope::class) class RoomMemberListNode @AssistedInject constructor( @@ -38,6 +37,7 @@ class RoomMemberListNode @AssistedInject constructor( interface Callback : Plugin { fun openRoomMemberDetails(roomMemberId: UserId) + fun openInviteMembers() } private val callbacks = plugins() @@ -48,6 +48,12 @@ class RoomMemberListNode @AssistedInject constructor( } } + private fun openInviteMembers() { + callbacks.forEach { + it.openInviteMembers() + } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -56,6 +62,7 @@ class RoomMemberListNode @AssistedInject constructor( modifier = modifier, onBackPressed = { navigateUp() }, onMemberSelected = this::openRoomMemberDetails, + onInvitePressed = this::openInviteMembers, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index ddf7864b96..36128bd638 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -18,6 +18,8 @@ package io.element.android.features.roomdetails.impl.members import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -27,12 +29,15 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.withContext import javax.inject.Inject class RoomMemberListPresenter @Inject constructor( + private val room: MatrixRoom, private val roomMemberListDataSource: RoomMemberListDataSource, private val coroutineDispatchers: CoroutineDispatchers, ) : Presenter { @@ -46,6 +51,9 @@ class RoomMemberListPresenter @Inject constructor( } var isSearchActive by rememberSaveable { mutableStateOf(false) } + val membersState by room.membersStateFlow.collectAsState() + val canInvite by getCanInvite(membersState = membersState) + LaunchedEffect(Unit) { withContext(coroutineDispatchers.io) { val members = roomMemberListDataSource.search("").groupBy { it.membership } @@ -80,6 +88,7 @@ class RoomMemberListPresenter @Inject constructor( searchQuery = searchQuery, searchResults = searchResults, isSearchActive = isSearchActive, + canInvite = canInvite, eventSink = { event -> when (event) { is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active @@ -88,5 +97,14 @@ class RoomMemberListPresenter @Inject constructor( }, ) } + + @Composable + private fun getCanInvite(membersState: MatrixRoomMembersState): State { + val canInvite = remember(membersState) { mutableStateOf(false) } + LaunchedEffect(membersState) { + canInvite.value = room.canInvite().getOrElse { false } + } + return canInvite + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt index 93e2142a19..f718db703b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -26,6 +26,7 @@ data class RoomMemberListState( val searchQuery: String, val searchResults: SearchBarResultState, val isSearchActive: Boolean, + val canInvite: Boolean, val eventSink: (RoomMemberListEvents) -> Unit, ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index 9bc8c00a1e..f1e6447a10 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -36,6 +36,7 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider Unit, + onInvitePressed: () -> Unit, onMemberSelected: (UserId) -> Unit, modifier: Modifier = Modifier, ) { @@ -79,7 +81,11 @@ fun RoomMemberListView( Scaffold( topBar = { if (!state.isSearchActive) { - RoomMemberListTopBar(onBackPressed = onBackPressed) + RoomMemberListTopBar( + canInvite = state.canInvite, + onBackPressed = onBackPressed, + onInvitePressed = onInvitePressed, + ) } } ) { padding -> @@ -192,8 +198,10 @@ private fun RoomMemberListItem( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun RoomMemberListTopBar( + canInvite: Boolean, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, + onInvitePressed: () -> Unit = {}, ) { CenterAlignedTopAppBar( modifier = modifier, @@ -205,6 +213,19 @@ private fun RoomMemberListTopBar( ) }, navigationIcon = { BackButton(onClick = onBackPressed) }, + actions = { + if (canInvite) { + TextButton( + modifier = Modifier.padding(horizontal = 8.dp), + onClick = onInvitePressed, + ) { + Text( + text = stringResource(StringR.string.action_invite), + fontSize = 16.sp, + ) + } + } + } ) } @@ -252,6 +273,7 @@ private fun ContentToPreview(state: RoomMemberListState) { RoomMemberListView( state = state, onBackPressed = {}, - onMemberSelected = {} + onMemberSelected = {}, + onInvitePressed = {}, ) } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index b3717f4244..8e97c42936 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -25,7 +25,6 @@ import io.element.android.features.roomdetails.impl.RoomDetailsEvent import io.element.android.features.roomdetails.impl.RoomDetailsPresenter import io.element.android.features.roomdetails.impl.RoomDetailsType import io.element.android.features.roomdetails.impl.members.aRoomMember -import io.element.android.features.roomdetails.impl.members.aRoomMemberList import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId @@ -33,7 +32,6 @@ import io.element.android.libraries.matrix.api.core.SessionId 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.MatrixRoomMembersState -import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange @@ -101,18 +99,19 @@ class RoomDetailsPresenterTests { room.givenRoomMembersState(MatrixRoomMembersState.Unknown) val initialState = awaitItem() Truth.assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized) + skipItems(1) room.givenRoomMembersState(MatrixRoomMembersState.Pending(null)) val loadingState = awaitItem() Truth.assertThat(loadingState.memberCount).isEqualTo(Async.Loading(null)) room.givenRoomMembersState(MatrixRoomMembersState.Error(error)) - //skipItems(1) + skipItems(1) val failureState = awaitItem() Truth.assertThat(failureState.memberCount).isEqualTo(Async.Failure(error, null)) room.givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) - //skipItems(1) + skipItems(1) val successState = awaitItem() Truth.assertThat(successState.memberCount).isEqualTo(Async.Success(1)) @@ -166,6 +165,8 @@ class RoomDetailsPresenterTests { presenter.present() }.test { val initialState = awaitItem() + skipItems(1) + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) val confirmationState = awaitItem() Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.PrivateRoom) @@ -182,6 +183,8 @@ class RoomDetailsPresenterTests { presenter.present() }.test { val initialState = awaitItem() + skipItems(1) + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) val confirmationState = awaitItem() Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.LastUserInRoom) @@ -198,6 +201,8 @@ class RoomDetailsPresenterTests { presenter.present() }.test { val initialState = awaitItem() + skipItems(1) + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) val confirmationState = awaitItem() Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.Generic) @@ -214,6 +219,8 @@ class RoomDetailsPresenterTests { presenter.present() }.test { val initialState = awaitItem() + skipItems(1) + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) cancelAndIgnoreRemainingEvents() @@ -235,6 +242,8 @@ class RoomDetailsPresenterTests { presenter.present() }.test { val initialState = awaitItem() + skipItems(1) + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) val errorState = awaitItem() Truth.assertThat(errorState.error).isNotNull() @@ -242,6 +251,50 @@ class RoomDetailsPresenterTests { Truth.assertThat(awaitItem().error).isNull() } } + + @Test + fun `present - initial state when user can invite others to room`() = runTest { + val room = aMatrixRoom().apply { + givenCanInviteResult(Result.success(true)) + } + val presenter = aRoomDetailsPresenter(room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + // Initially false + Truth.assertThat(awaitItem().canInvite).isFalse() + // Then the asynchronous check completes and it becomes true + Truth.assertThat(awaitItem().canInvite).isTrue() + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - initial state when user can not invite others to room`() = runTest { + val room = aMatrixRoom().apply { + givenCanInviteResult(Result.success(false)) + } + val presenter = aRoomDetailsPresenter(room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + Truth.assertThat(awaitItem().canInvite).isFalse() + } + } + + @Test + fun `present - initial state when canInvite errors`() = runTest { + val room = aMatrixRoom().apply { + givenCanInviteResult(Result.failure(Throwable("Whoops"))) + } + val presenter = aRoomDetailsPresenter(room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + Truth.assertThat(awaitItem().canInvite).isFalse() + } + } } fun aMatrixClient( diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt new file mode 100644 index 0000000000..49ecff7d3b --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt @@ -0,0 +1,331 @@ +/* + * 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.features.roomdetails.impl.invite + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomdetails.aMatrixRoom +import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource +import io.element.android.features.roomdetails.impl.members.aRoomMember +import io.element.android.features.roomdetails.impl.members.aRoomMemberList +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import io.element.android.libraries.usersearch.test.FakeUserRepository +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class RoomInviteMembersPresenterTest { + + @Test + fun `present - initial state has no results and no search`() = runTest { + val presenter = RoomInviteMembersPresenter( + userRepository = FakeUserRepository(), + roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + + assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java) + assertThat(initialState.isSearchActive).isFalse() + assertThat(initialState.canInvite).isFalse() + assertThat(initialState.searchQuery).isEmpty() + + skipItems(1) + } + } + + @Test + fun `present - updates search active state`() = runTest { + val presenter = RoomInviteMembersPresenter( + userRepository = FakeUserRepository(), + roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + initialState.eventSink(RoomInviteMembersEvents.OnSearchActiveChanged(true)) + + val resultState = awaitItem() + assertThat(resultState.isSearchActive).isTrue() + } + } + + @Test + fun `present - performs search and handles no results`() = runTest { + val repository = FakeUserRepository() + val presenter = RoomInviteMembersPresenter( + userRepository = repository, + roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query")) + skipItems(1) + + assertThat(repository.providedQuery).isEqualTo("some query") + repository.emitResult(emptyList()) + skipItems(1) + + val resultState = awaitItem() + assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.NoResults::class.java) + } + } + + @Test + fun `present - performs search and handles user results`() = runTest { + val repository = FakeUserRepository() + val presenter = RoomInviteMembersPresenter( + userRepository = repository, + roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query")) + skipItems(1) + + assertThat(repository.providedQuery).isEqualTo("some query") + repository.emitResult(aMatrixUserList()) + skipItems(1) + + val resultState = awaitItem() + assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + + val expectedUsers = aMatrixUserList() + val users = resultState.searchResults.users() + expectedUsers.forEachIndexed { index, matrixUser -> + assertThat(users[index].matrixUser).isEqualTo(matrixUser) + assertThat(users[index].isAlreadyInvited).isFalse() + assertThat(users[index].isAlreadyJoined).isFalse() + assertThat(users[index].isSelected).isFalse() + } + } + } + + @Test + fun `present - performs search and handles membership state of existing users`() = runTest { + val userList = aMatrixUserList() + val joinedUser = userList[0] + val invitedUser = userList[1] + + val repository = FakeUserRepository() + val presenter = RoomInviteMembersPresenter( + userRepository = repository, + roomMemberListDataSource = createDataSource(FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(listOf( + aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN), + aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE), + ))) + }), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query")) + skipItems(1) + + assertThat(repository.providedQuery).isEqualTo("some query") + repository.emitResult(aMatrixUserList()) + skipItems(1) + + val resultState = awaitItem() + assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + + val users = resultState.searchResults.users() + + // The result that matches a user with JOINED membership is marked as such + val userWhoShouldBeJoined = users.find { it.matrixUser == joinedUser } + assertThat(userWhoShouldBeJoined).isNotNull() + assertThat(userWhoShouldBeJoined?.isAlreadyJoined).isTrue() + assertThat(userWhoShouldBeJoined?.isAlreadyInvited).isFalse() + + // The result that matches a user with INVITED membership is marked as such + val userWhoShouldBeInvited = users.find { it.matrixUser == invitedUser } + assertThat(userWhoShouldBeInvited).isNotNull() + assertThat(userWhoShouldBeInvited?.isAlreadyJoined).isFalse() + assertThat(userWhoShouldBeInvited?.isAlreadyInvited).isTrue() + + // All other users are neither joined nor invited + val otherUsers = users.minus(userWhoShouldBeInvited!!).minus(userWhoShouldBeJoined!!) + assertThat(otherUsers.none { it.isAlreadyInvited }).isTrue() + assertThat(otherUsers.none { it.isAlreadyJoined }).isTrue() + } + } + + @Test + fun `present - toggle users updates selected user state`() = runTest { + val repository = FakeUserRepository() + val presenter = RoomInviteMembersPresenter( + userRepository = repository, + roomMemberListDataSource = createDataSource(), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + // When we toggle a user not in the list, they are added + initialState.eventSink(RoomInviteMembersEvents.ToggleUser(aMatrixUser())) + assertThat(awaitItem().selectedUsers).containsExactly(aMatrixUser()) + + // Toggling a different user also adds them + initialState.eventSink(RoomInviteMembersEvents.ToggleUser(aMatrixUser(id = A_USER_ID_2.value))) + assertThat(awaitItem().selectedUsers).containsExactly(aMatrixUser(), aMatrixUser(id = A_USER_ID_2.value)) + + // Toggling the first user removes them + initialState.eventSink(RoomInviteMembersEvents.ToggleUser(aMatrixUser())) + assertThat(awaitItem().selectedUsers).containsExactly(aMatrixUser(id = A_USER_ID_2.value)) + } + } + + @Test + fun `present - selected users appear as such in search results`() = runTest { + val repository = FakeUserRepository() + val presenter = RoomInviteMembersPresenter( + userRepository = repository, + roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + val selectedUser = aMatrixUser() + + initialState.eventSink(RoomInviteMembersEvents.ToggleUser(selectedUser)) + + initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query")) + skipItems(1) + + assertThat(repository.providedQuery).isEqualTo("some query") + repository.emitResult(aMatrixUserList() + selectedUser) + skipItems(2) + + val resultState = awaitItem() + assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + + val users = resultState.searchResults.users() + + // The one user we have previously toggled is marked as selected + val shouldBeSelectedUser = users.find { it.matrixUser == selectedUser } + assertThat(shouldBeSelectedUser).isNotNull() + assertThat(shouldBeSelectedUser?.isSelected).isTrue() + + // And no others are + val allOtherUsers = users.minus(shouldBeSelectedUser!!) + assertThat(allOtherUsers.none { it.isSelected }).isTrue() + } + } + + + @Test + fun `present - toggling a user updates existing search results`() = runTest { + val repository = FakeUserRepository() + val presenter = RoomInviteMembersPresenter( + userRepository = repository, + roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + val selectedUser = aMatrixUser() + + // Given a query is made + initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query")) + skipItems(1) + + assertThat(repository.providedQuery).isEqualTo("some query") + repository.emitResult(aMatrixUserList() + selectedUser) + skipItems(2) + + // And then a user is toggled + initialState.eventSink(RoomInviteMembersEvents.ToggleUser(selectedUser)) + skipItems(1) + val resultState = awaitItem() + + // The results are updated... + assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + val users = resultState.searchResults.users() + + // The one user we have now toggled is marked as selected + val shouldBeSelectedUser = users.find { it.matrixUser == selectedUser } + assertThat(shouldBeSelectedUser).isNotNull() + assertThat(shouldBeSelectedUser?.isSelected).isTrue() + + // And no others are + val allOtherUsers = users.minus(shouldBeSelectedUser!!) + assertThat(allOtherUsers.none { it.isSelected }).isTrue() + } + } + + private fun createDataSource( + matrixRoom: MatrixRoom = aMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + }, + coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers() + ) = RoomMemberListDataSource(matrixRoom, coroutineDispatchers) + + private fun SearchBarResultState>.users() = + (this as? SearchBarResultState.Results>)?.results.orEmpty() +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 73c873701e..9625a167f7 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -32,6 +32,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -113,6 +114,54 @@ class RoomMemberListPresenterTests { } } + + @Test + fun `present - asynchronously sets canInvite when user has correct power level`() = runTest { + val presenter = createPresenter( + matrixRoom = FakeMatrixRoom().apply { + givenCanInviteResult(Result.success(true)) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val loadedState = awaitItem() + Truth.assertThat(loadedState.canInvite).isTrue() + } + } + + @Test + fun `present - asynchronously sets canInvite when user does not have correct power level`() = runTest { + val presenter = createPresenter( + matrixRoom = FakeMatrixRoom().apply { + givenCanInviteResult(Result.success(false)) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val loadedState = awaitItem() + Truth.assertThat(loadedState.canInvite).isFalse() + } + } + + @Test + fun `present - asynchronously sets canInvite when power level check fails`() = runTest { + val presenter = createPresenter( + matrixRoom = FakeMatrixRoom().apply { + givenCanInviteResult(Result.failure(Throwable("Eek"))) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val loadedState = awaitItem() + Truth.assertThat(loadedState.canInvite).isFalse() + } + } } @ExperimentalCoroutinesApi @@ -125,6 +174,7 @@ private fun createDataSource( @ExperimentalCoroutinesApi private fun createPresenter( + matrixRoom: MatrixRoom = FakeMatrixRoom(), roomMemberListDataSource: RoomMemberListDataSource = createDataSource(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers() -) = RoomMemberListPresenter(roomMemberListDataSource, coroutineDispatchers) +) = RoomMemberListPresenter(matrixRoom, roomMemberListDataSource, coroutineDispatchers) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt index 8d91404d29..3338a218cb 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt @@ -57,6 +57,7 @@ fun SearchBar( placeHolderTitle: String, modifier: Modifier = Modifier, enabled: Boolean = true, + showBackButton: Boolean = true, resultState: SearchBarResultState = SearchBarResultState.NotSearching(), shape: Shape = SearchBarDefaults.inputFieldShape, tonalElevation: Dp = SearchBarDefaults.Elevation, @@ -87,7 +88,7 @@ fun SearchBar( modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine) ) }, - leadingIcon = if (active) { + leadingIcon = if (showBackButton && active) { { BackButton(onClick = { onActiveChange(false) }) } } else { null @@ -179,6 +180,16 @@ internal fun SearchBarPreviewActiveWithQuery() = ElementThemedPreview { ) } +@Preview(group = PreviewGroup.Search) +@Composable +internal fun SearchBarPreviewActiveWithQueryNoBackButton() = ElementThemedPreview { + ContentToPreview( + query = "search term", + active = true, + showBackButton = false, + ) +} + @Preview(group = PreviewGroup.Search) @Composable internal fun SearchBarPreviewActiveWithNoResults() = ElementThemedPreview { @@ -212,6 +223,7 @@ internal fun SearchBarPreviewActiveWithContent() = ElementThemedPreview { private fun ContentToPreview( query: String = "", active: Boolean = false, + showBackButton: Boolean = true, resultState: SearchBarResultState = SearchBarResultState.NotSearching(), contentPrefix: @Composable ColumnScope.() -> Unit = {}, contentSuffix: @Composable ColumnScope.() -> Unit = {}, @@ -221,6 +233,7 @@ private fun ContentToPreview( query = query, active = active, resultState = resultState, + showBackButton = showBackButton, onQueryChange = {}, onActiveChange = {}, placeHolderTitle = "Search for things", diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 90b55a3a42..5cf9375e0a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -85,4 +85,8 @@ interface MatrixRoom : Closeable { suspend fun acceptInvitation(): Result suspend fun rejectInvitation(): Result + + suspend fun inviteUserById(id: UserId): Result + + suspend fun canInvite(): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index ae35449a8e..63bdf6fccc 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.SlidingSyncRoom import org.matrix.rustcomponents.sdk.UpdateSummary import org.matrix.rustcomponents.sdk.genTransactionId @@ -209,6 +210,18 @@ class RustMatrixRoom( } } + override suspend fun inviteUserById(id: UserId): Result = withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.inviteUserById(id.value) + } + } + + override suspend fun canInvite(): Result = withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.member(sessionId.value).use(RoomMember::canInvite) + } + } + override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result { return runCatching { innerRoom.sendImage(file.path, thumbnailFile.path, imageInfo.map()) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 6f64db76ef..0019589b94 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -152,6 +152,7 @@ class RustMatrixTimeline( RequiredState(key = "m.room.canonical_alias", value = ""), RequiredState(key = "m.room.topic", value = ""), RequiredState(key = "m.room.join_rules", value = ""), + RequiredState(key = "m.room.power_levels", value = ""), ), timelineLimit = null ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index a790812137..5b90f1b915 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -59,6 +59,8 @@ class FakeMatrixRoom( private var updateMembersResult: Result = Result.success(Unit) private var acceptInviteResult = Result.success(Unit) private var rejectInviteResult = Result.success(Unit) + private var inviteUserResult = Result.success(Unit) + private var canInviteResult = Result.success(true) private var sendMediaResult = Result.success(Unit) var sendMediaCount = 0 private set @@ -69,6 +71,9 @@ class FakeMatrixRoom( var isInviteRejected: Boolean = false private set + var invitedUserId: UserId? = null + private set + private var leaveRoomError: Throwable? = null override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) @@ -136,6 +141,15 @@ class FakeMatrixRoom( return rejectInviteResult } + override suspend fun inviteUserById(id: UserId): Result { + invitedUserId = id + return inviteUserResult + } + + override suspend fun canInvite(): Result { + return canInviteResult + } + override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result = sendMediaResult.also { sendMediaCount++ } override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result = sendMediaResult.also { sendMediaCount++ } @@ -174,6 +188,14 @@ class FakeMatrixRoom( rejectInviteResult = result } + fun givenInviteUserResult(result: Result) { + inviteUserResult = result + } + + fun givenCanInviteResult(result: Result) { + canInviteResult = result + } + fun givenIgnoreResult(result: Result) { ignoreResult = result } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 40468f8a05..2736bdebd2 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -69,6 +69,7 @@ "File" "GIF" "Image" + "Leaving room" "Link copied to clipboard" "Loading…" "Message" @@ -98,6 +99,8 @@ "Suggestions" "Topic" "Unable to decrypt" + "We were unable to successfully send invites to one or more users." + "Unable to send invite(s)" "Unsupported event" "Username" "Verification cancelled" @@ -162,4 +165,4 @@ "You can read all our terms %1$s." "here" "Block user" - + \ No newline at end of file diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index e7023ad88e..c433ed2e07 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -95,6 +95,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { fun DependencyHandlerScope.allServicesImpl() { implementation(project(":services:analytics:noop")) + implementation(project(":services:apperror:impl")) implementation(project(":services:appnavstate:impl")) implementation(project(":services:toolbox:impl")) } diff --git a/services/apperror/api/build.gradle.kts b/services/apperror/api/build.gradle.kts new file mode 100644 index 0000000000..94970d9774 --- /dev/null +++ b/services/apperror/api/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.services.apperror.api" +} + +dependencies { + implementation(libs.coroutines.core) +} diff --git a/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt new file mode 100644 index 0000000000..c808ebe503 --- /dev/null +++ b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt @@ -0,0 +1,29 @@ +/* + * 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.services.apperror.api + +sealed interface AppErrorState { + + object NoError : AppErrorState + + data class Error( + val title: String, + val body: String, + val dismiss: () -> Unit, + ) : AppErrorState + +} diff --git a/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateProvider.kt b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateProvider.kt new file mode 100644 index 0000000000..50d857645e --- /dev/null +++ b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateProvider.kt @@ -0,0 +1,23 @@ +/* + * 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.services.apperror.api + +fun aAppErrorState() = AppErrorState.Error( + title = "An error occurred", + body = "Something went wrong, and the details of that would go here.", + dismiss = {}, +) diff --git a/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateService.kt b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateService.kt new file mode 100644 index 0000000000..b1f9b97ac2 --- /dev/null +++ b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateService.kt @@ -0,0 +1,27 @@ +/* + * 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.services.apperror.api + +import kotlinx.coroutines.flow.StateFlow + +interface AppErrorStateService { + + val appErrorStateFlow: StateFlow + + fun showError(title: String, body: String) + +} diff --git a/services/apperror/impl/build.gradle.kts b/services/apperror/impl/build.gradle.kts new file mode 100644 index 0000000000..285577de9a --- /dev/null +++ b/services/apperror/impl/build.gradle.kts @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 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. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.ksp) + alias(libs.plugins.anvil) +} + +anvil { + generateDaggerFactories.set(true) +} + +android { + namespace = "io.element.android.services.apperror.impl" +} + +dependencies { + anvil(projects.anvilcodegen) + implementation(libs.dagger) + implementation(projects.libraries.core) + implementation(projects.libraries.di) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + implementation(projects.anvilannotations) + + implementation(libs.coroutines.core) + implementation(libs.androidx.corektx) + + api(projects.services.apperror.api) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.test.turbine) + testImplementation(libs.test.truth) + + ksp(libs.showkase.processor) +} diff --git a/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt b/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt new file mode 100644 index 0000000000..ac5d4ee75d --- /dev/null +++ b/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt @@ -0,0 +1,66 @@ +/* + * 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.services.apperror.impl + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.services.apperror.api.AppErrorState +import io.element.android.services.apperror.api.aAppErrorState + +@Composable +fun AppErrorView( + state: AppErrorState, +) { + if (state is AppErrorState.Error) { + AppErrorViewContent( + title = state.title, + body = state.body, + onDismiss = state.dismiss, + ) + } +} + +@Composable +fun AppErrorViewContent( + title: String, + body: String, + onDismiss: () -> Unit = { }, +) { + ErrorDialog( + title = title, + content = body, + onDismiss = onDismiss, + ) +} + +@Preview +@Composable +internal fun AppErrorViewLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun AppErrorViewDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + AppErrorView( + state = aAppErrorState() + ) +} diff --git a/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateService.kt b/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateService.kt new file mode 100644 index 0000000000..813c00cd65 --- /dev/null +++ b/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateService.kt @@ -0,0 +1,44 @@ +/* + * 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.services.apperror.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import io.element.android.services.apperror.api.AppErrorState +import io.element.android.services.apperror.api.AppErrorStateService +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) +class DefaultAppErrorStateService @Inject constructor() : AppErrorStateService { + + private val currentAppErrorState = MutableStateFlow(AppErrorState.NoError) + override val appErrorStateFlow: StateFlow = currentAppErrorState + + override fun showError(title: String, body: String) { + currentAppErrorState.value = AppErrorState.Error( + title = title, + body = body, + dismiss = { + currentAppErrorState.value = AppErrorState.NoError + }, + ) + } +} diff --git a/services/apperror/impl/src/test/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateServiceTest.kt b/services/apperror/impl/src/test/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateServiceTest.kt new file mode 100644 index 0000000000..71c20aa8a2 --- /dev/null +++ b/services/apperror/impl/src/test/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateServiceTest.kt @@ -0,0 +1,72 @@ +/* + * 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.services.apperror.impl + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.services.apperror.api.AppErrorState +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class DefaultAppErrorStateServiceTest { + + @Test + fun `initial value is no error`() = runTest { + val service = DefaultAppErrorStateService() + + service.appErrorStateFlow.test { + val state = awaitItem() + assertThat(state).isInstanceOf(AppErrorState.NoError::class.java) + } + } + + @Test + fun `showError - emits value`() = runTest { + val service = DefaultAppErrorStateService() + + service.appErrorStateFlow.test { + skipItems(1) + + service.showError("Title", "Body") + val state = awaitItem() + assertThat(state).isInstanceOf(AppErrorState.Error::class.java) + + val errorState = state as AppErrorState.Error + assertThat(errorState.title).isEqualTo("Title") + assertThat(errorState.body).isEqualTo("Body") + } + } + + @Test + fun `dismiss - clears value`() = runTest { + val service = DefaultAppErrorStateService() + + service.appErrorStateFlow.test { + skipItems(1) + + service.showError("Title", "Body") + val state = awaitItem() + assertThat(state).isInstanceOf(AppErrorState.Error::class.java) + + val errorState = state as AppErrorState.Error + errorState.dismiss() + + assertThat(awaitItem()).isInstanceOf(AppErrorState.NoError::class.java) + } + } + +} diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 8b87a5a644..ebc59dec6a 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56148beb26b35c8309190271b43fc225e11b90b8b849a8e60abf98b6ab663c1b -size 101010 +oid sha256:439a2d9e9497486e86b63fe42402f1c2ee33b15695e47312e18867530d041dad +size 96539 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index ba7682c859..dd593bdc3e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fa60afaf7ab23f66622d7d77e168db8b9d8bc15d178e8adb63f944de3cc29df -size 96242 +oid sha256:255b99dea5c1da9d466533d510d7c0abe39a79cdc42b579cfa186176e9c6a49d +size 91991 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ee9f0fb113 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:516e972069292f31625847ca39de06ea402ce1606c97d84f930c18eaa2448cfa +size 13759 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a1094a6fa6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b387f10903926ba9ed30b7d213314388eebf23743cbbdbcf4b613cf3136f64aa +size 41524 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..79b48dd535 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0af8c7149fd4266c68c0e4b8b9a699bc0b3fc0b05fea3f2e2544abc884f70c5b +size 11988 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ff95c9ed19 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8eb2fcdb57b7f1ba28990f9576fad6dce156fe0c5c9ae491797040fc22ae4f7b +size 39516 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f7f16c2b8a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e4d3bc53e6a86957218247b614be6bbc78cfb35ce49565f2243f50d6ef94fe3 +size 14216 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0cd4f9c3e0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:213777e4ace6f9c33209db37bffff2ca6dba13e540e9d07be82239d261ee8e3f +size 63951 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..57c7799447 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ddec11fd636d345c49cf981ce7498ac2d94027d4b223b257b64288bb0254766 +size 13328 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1db9b42586 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da5ba4637d999c8654e38a86ad0b26c75a6fe10a6988eb821b24cf91da2274e5 +size 39070 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6648389b0e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c57c20fe79d0dbb0ca25571fa2ab3ba6e06f3a9c6634e96fac3ed0155c2ea106 +size 11160 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5b6a14ea66 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e43279132a7536516cf267599f877f7c729f76be51d6319944f74c57af0e282 +size 36493 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fd948f750d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:933c5ac4478699e9e713af559025ab675a8bdc93b15c42ff27fe5525fa3b668a +size 13166 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a35148eff1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a66a39bf18ba35abfc87dea22ca019eea98727bf246142cb034649c5b1772bd +size 61041 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_2,NEXUS_5,1.0,en].png index d03094bab0..28c2f7631d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49e7533e6afba903219942193e0cc5cfbe2f67ba3b57516f77c259e7c42e8e3f -size 11813 +oid sha256:bbe60787ea821d25168d4a6d75000629c9c0d505a2c8eec9ba3af57790bf4b0a +size 12863 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_3,NEXUS_5,1.0,en].png index f13a65f8e2..d03094bab0 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89ea65099fb4981bbeb24e49afb7400b0e8da79e3b783e198519ba1e970404a8 -size 8317 +oid sha256:49e7533e6afba903219942193e0cc5cfbe2f67ba3b57516f77c259e7c42e8e3f +size 11813 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_4,NEXUS_5,1.0,en].png index 74a7f599cd..f13a65f8e2 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a8ba207cf61c56b64c6855d04c8a30def0b3daf325adf57edd45b40981ed745 -size 7733 +oid sha256:89ea65099fb4981bbeb24e49afb7400b0e8da79e3b783e198519ba1e970404a8 +size 8317 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_5,NEXUS_5,1.0,en].png index 22fe768986..74a7f599cd 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:070c93168058fded5a76e49050f6c4554c7fe483a19aae13c1af4426a6b575f1 -size 30582 +oid sha256:9a8ba207cf61c56b64c6855d04c8a30def0b3daf325adf57edd45b40981ed745 +size 7733 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_6,NEXUS_5,1.0,en].png index 0f9a5a79cc..22fe768986 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5b879b74654fdad0638f432a71adfc9186fbe00dafd5d963a3affb33ec8c5c8 -size 12841 +oid sha256:070c93168058fded5a76e49050f6c4554c7fe483a19aae13c1af4426a6b575f1 +size 30582 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0f9a5a79cc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5b879b74654fdad0638f432a71adfc9186fbe00dafd5d963a3affb33ec8c5c8 +size 12841 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_2,NEXUS_5,1.0,en].png index 3d261cb40b..fbe3e96f54 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c3abebbe9e55706af5f4d1089e4308b0e975258a9d6a0e1793a4208b36c9c93 -size 11754 +oid sha256:925e60112749ce49cf1c2d855406860375357e4717cb7487d3b8bb9a94d7e816 +size 12882 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_3,NEXUS_5,1.0,en].png index eb23dcec96..3d261cb40b 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8de2f28cf9918a7eddcc1311c34ba0376242a4708724b57689df000b7480524 -size 8197 +oid sha256:9c3abebbe9e55706af5f4d1089e4308b0e975258a9d6a0e1793a4208b36c9c93 +size 11754 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_4,NEXUS_5,1.0,en].png index e2e4a33e67..eb23dcec96 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:687133e4729ea42382ac0b76af6ceaf8b20cbc65923412fb97b077d2475c70f7 -size 7514 +oid sha256:b8de2f28cf9918a7eddcc1311c34ba0376242a4708724b57689df000b7480524 +size 8197 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_5,NEXUS_5,1.0,en].png index 1b35815820..e2e4a33e67 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9dd67135f57c6e8a336e238384b02cd6068770b84f429f357d03d5e1e4c22ab6 -size 29286 +oid sha256:687133e4729ea42382ac0b76af6ceaf8b20cbc65923412fb97b077d2475c70f7 +size 7514 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_6,NEXUS_5,1.0,en].png index 787c71d87e..1b35815820 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:534cba0c13aa52b3557ab8df854c521dd13b82e5a1550d73abcef12925e389da -size 11878 +oid sha256:9dd67135f57c6e8a336e238384b02cd6068770b84f429f357d03d5e1e4c22ab6 +size 29286 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..787c71d87e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:534cba0c13aa52b3557ab8df854c521dd13b82e5a1550d73abcef12925e389da +size 11878 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png index bb0a5b2211..3cd7e20f05 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a755d1793d5a77244abe66b8a72521d64c46c102e9e9c60d1839d793301526d -size 39373 +oid sha256:72f159556f6ccee83c65589b882b3ac9ca9d4f4de8e611f31e19a524e22eab8e +size 37569 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png index ded656a069..090890f1ad 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04ffb414eeb75e4fb4187777be04c3fc3a9a0967c81a2150ea2d4f58ef4212bc -size 30599 +oid sha256:6b1ac0c5e9fc3ecb9870ed3322418c26e26ec0873806d3eb7a89b2a51b39c1f1 +size 28563 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png index 1a92b42c6b..48ead02bc6 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5ccb63879a3b9a4d37a38903a5b81cf1a185afc638f2d0260b6a6fe68aa2419 -size 28779 +oid sha256:aa8e818a0c6175cc40b888f8c5a093655b8a352cc2b90e7159d9317de8f68e8f +size 26778 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png index 6a8e4b6ac0..29b6bcc2c0 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6b5488817b1262bc17800567b3634861230f6db3b81fbe1e91f26d6795a44ae -size 36828 +oid sha256:54b897538574ecb5a6e7a13b0f3ae8d0495e8db4667f1878dfc0ca354d0faf47 +size 34903 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png index 622b3a6def..06ff0e03c3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d1d7c27b6c1b3ca495fe794ca888e55f013b8c99b716f4571f8c3c278ed85d7 -size 38995 +oid sha256:1d5ae33b866c962c0a444b6893dc602b99943a2305a3c3e9695219c1a33388bb +size 37208 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bb0a5b2211 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a755d1793d5a77244abe66b8a72521d64c46c102e9e9c60d1839d793301526d +size 39373 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png index 85b1b93a66..3e9cbb975a 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:333798d610045630944e53c4c43bc401781cc5499ad8c085037239a93de6eccc -size 36950 +oid sha256:8912f6213b14f6380359582e96901ee5a1dcdf9f939de3ac8d5bbb6a62f25eaa +size 35190 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png index c0a142af4b..4d1b84cdb3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0206a86b0db1d4c2a984435fda5b70335fa3f005cc4c49a3da7d1f3f80fb04e6 -size 29057 +oid sha256:ad68e0852e7742b58d976a1d8ca2e2da66bf5477c0876ecc9e2c1007cf4b843f +size 27197 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png index 8c2ed2d853..a4c174b08f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d55124590cfc79d4c85a35de16223ff7b7821c32742670ea12af0bea05a6ef87 -size 27081 +oid sha256:048309d5c82ff3ea6dcdd1e5bc81103b5d9147083089f7a006fce840b1d23867 +size 25445 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png index 7fe6e59dd6..4575ec4932 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fadd36fcc359c5c20d21f1525807d07487409f736e9b82d79d261c1e1a48dd4e -size 34406 +oid sha256:ff28aa87583eb9ec9966f761deec56757c4cdd68988a37e46af8881affe49802 +size 32555 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png index f9cdf0cd9c..b61de7b17c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88a68971b697354a92f04de8b73734d1c4ac10792b212dd34d16e16f8dde5ad5 -size 36572 +oid sha256:3e5ee6f7d37a16a3472eb2a92d611afe270852131f2f330bb3f5ee7db06c1453 +size 34804 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..85b1b93a66 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:333798d610045630944e53c4c43bc401781cc5499ad8c085037239a93de6eccc +size 36950 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithQueryNoBackButton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithQueryNoBackButton_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..13e9fa77a0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithQueryNoBackButton_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79739bfd4bd1970e3ea7d5f938c6303f2aae7a4e332651651c26481609d166d4 +size 7655