From 0dee0784ba7ca76e532e47075b08e250ee2a8a56 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 25 May 2023 08:42:44 +0200 Subject: [PATCH] Room list contextual menu (#427) - Adds `ModalBottomSheet` to our design components (it wraps the homonimous Material3 one). - Adds a bottom sheet to the Room list using the aforementioned design component. - Adds navigation from the room list to a room detail (context menu "Settings" action). - Consolidates the "leave room flow" into a new `leaveroom` module used by both the room list and the room details. - Adds progress indicator to the leave room flow - Uses new `leaveroom` module in `roomdetails` module too. Parent issue: - https://github.com/vector-im/element-x-android/issues/261 --- .../android/appnav/LoggedInFlowNode.kt | 11 +- .../io/element/android/appnav/RoomFlowNode.kt | 4 +- changelog.d/427.feature | 1 + features/leaveroom/api/build.gradle.kts | 31 +++ .../features/leaveroom/api/LeaveRoomEvent.kt | 26 ++ .../leaveroom/api/LeaveRoomPresenter.kt | 25 ++ .../features/leaveroom/api/LeaveRoomState.kt | 43 +++ .../leaveroom/api/LeaveRoomStateProvider.kt | 58 ++++ .../features/leaveroom/api/LeaveRoomView.kt | 131 +++++++++ features/leaveroom/fake/build.gradle.kts | 44 ++++ .../leaveroom/fake/LeaveRoomPresenterFake.kt | 44 ++++ .../fake/LeaveRoomPresenterFakeModule.kt | 30 +++ features/leaveroom/impl/build.gradle.kts | 46 ++++ .../leaveroom/impl/LeaveRoomPresenterImpl.kt | 127 +++++++++ .../impl/LeaveRoomPresenterImplModule.kt | 30 +++ .../impl/LeaveRoomPresenterImplTest.kt | 248 ++++++++++++++++++ features/roomdetails/impl/build.gradle.kts | 2 + .../roomdetails/impl/RoomDetailsEvent.kt | 4 +- .../roomdetails/impl/RoomDetailsPresenter.kt | 57 +--- .../roomdetails/impl/RoomDetailsState.kt | 24 +- .../impl/RoomDetailsStateProvider.kt | 4 +- .../roomdetails/impl/RoomDetailsView.kt | 44 +--- .../roomdetails/RoomDetailsPresenterTests.kt | 106 +------- .../roomlist/api/RoomListEntryPoint.kt | 1 + features/roomlist/impl/build.gradle.kts | 2 + .../roomlist/impl/RoomListContextMenu.kt | 148 +++++++++++ .../features/roomlist/impl/RoomListEvents.kt | 6 + .../features/roomlist/impl/RoomListNode.kt | 9 +- .../roomlist/impl/RoomListPresenter.kt | 16 ++ .../features/roomlist/impl/RoomListState.kt | 16 +- .../roomlist/impl/RoomListStateProvider.kt | 6 + .../features/roomlist/impl/RoomListView.kt | 57 +++- .../impl/components/RoomSummaryRow.kt | 16 +- .../impl/model/RoomListRoomSummary.kt | 2 +- .../model/RoomListRoomSummaryPlaceholders.kt | 3 +- .../roomlist/impl/search/RoomListSearch.kt | 15 +- .../roomlist/impl/RoomListPresenterTests.kt | 94 +++++++ .../libraries/designsystem/VectorIcons.kt | 1 + .../theme/components/ModalBottomSheet.kt | 103 ++++++++ .../src/main/res/drawable/ic_door_open_24.xml | 10 + samples/minimal/build.gradle.kts | 1 + .../android/samples/minimal/RoomListScreen.kt | 12 +- ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...tentDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...entLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_8,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_8,NEXUS_5,1.0,en].png | 3 + ...heetDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...eetLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + 60 files changed, 1462 insertions(+), 250 deletions(-) create mode 100644 changelog.d/427.feature create mode 100644 features/leaveroom/api/build.gradle.kts create mode 100644 features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt create mode 100644 features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomPresenter.kt create mode 100644 features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt create mode 100644 features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt create mode 100644 features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt create mode 100644 features/leaveroom/fake/build.gradle.kts create mode 100644 features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFake.kt create mode 100644 features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFakeModule.kt create mode 100644 features/leaveroom/impl/build.gradle.kts create mode 100644 features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt create mode 100644 features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplModule.kt create mode 100644 features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt create mode 100644 libraries/designsystem/src/main/res/drawable/ic_door_open_24.xml create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_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.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentDarkPreview_0_null,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.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentLightPreview_0_null,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.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_8,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.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_8,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_BottomSheets_ModalBottomSheetDarkPreview_0_null,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_BottomSheets_ModalBottomSheetLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index b8c2c7742a..62c58caadb 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -135,7 +135,10 @@ class LoggedInFlowNode @AssistedInject constructor( object RoomList : NavTarget @Parcelize - data class Room(val roomId: RoomId) : NavTarget + data class Room( + val roomId: RoomId, + val initialElement: RoomFlowNode.NavTarget = RoomFlowNode.NavTarget.Messages + ) : NavTarget @Parcelize object Settings : NavTarget @@ -176,6 +179,10 @@ class LoggedInFlowNode @AssistedInject constructor( override fun onInvitesClicked() { backstack.push(NavTarget.InviteList) } + + override fun onRoomSettingsClicked(roomId: RoomId) { + backstack.push(NavTarget.Room(roomId, initialElement = RoomFlowNode.NavTarget.RoomDetails)) + } } roomListEntryPoint .nodeBuilder(this, buildContext) @@ -193,7 +200,7 @@ class LoggedInFlowNode @AssistedInject constructor( } } else { val nodeLifecycleCallbacks = plugins() - val inputs = RoomFlowNode.Inputs(room) + val inputs = RoomFlowNode.Inputs(room, initialElement = navTarget.initialElement) createNode(buildContext, plugins = listOf(inputs) + nodeLifecycleCallbacks) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index 2b33e37c7e..0493ae7db1 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -59,7 +59,7 @@ class RoomFlowNode @AssistedInject constructor( roomMembershipObserver: RoomMembershipObserver, ) : BackstackNode( backstack = BackStack( - initialElement = NavTarget.Messages, + initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialElement, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -73,6 +73,7 @@ class RoomFlowNode @AssistedInject constructor( data class Inputs( val room: MatrixRoom, + val initialElement: NavTarget = NavTarget.Messages, ) : NodeInputs private val inputs: Inputs = inputs() @@ -98,6 +99,7 @@ class RoomFlowNode @AssistedInject constructor( navigateUp() } .launchIn(lifecycleScope) + inputs() } private fun fetchRoomMembers() = lifecycleScope.launch { diff --git a/changelog.d/427.feature b/changelog.d/427.feature new file mode 100644 index 0000000000..5aad0dcec8 --- /dev/null +++ b/changelog.d/427.feature @@ -0,0 +1 @@ +Room list contextual menu diff --git a/features/leaveroom/api/build.gradle.kts b/features/leaveroom/api/build.gradle.kts new file mode 100644 index 0000000000..83ca28b39a --- /dev/null +++ b/features/leaveroom/api/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * 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. + */ +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.ksp) +} + +android { + namespace = "io.element.android.features.leaveroom.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + implementation(projects.libraries.matrix.api) + ksp(libs.showkase.processor) +} diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt new file mode 100644 index 0000000000..d1a3369ac6 --- /dev/null +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt @@ -0,0 +1,26 @@ +/* + * 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.leaveroom.api + +import io.element.android.libraries.matrix.api.core.RoomId + +sealed interface LeaveRoomEvent { + data class ShowConfirmation(val roomId: RoomId) : LeaveRoomEvent + object HideConfirmation : LeaveRoomEvent + data class LeaveRoom(val roomId: RoomId) : LeaveRoomEvent + object HideError : LeaveRoomEvent +} diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomPresenter.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomPresenter.kt new file mode 100644 index 0000000000..dd1f83691e --- /dev/null +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomPresenter.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.leaveroom.api + +import androidx.compose.runtime.Composable +import io.element.android.libraries.architecture.Presenter + +interface LeaveRoomPresenter : Presenter { + @Composable + override fun present(): LeaveRoomState +} diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt new file mode 100644 index 0000000000..7cb9926677 --- /dev/null +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt @@ -0,0 +1,43 @@ +/* + * 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.leaveroom.api + +import io.element.android.libraries.matrix.api.core.RoomId + +data class LeaveRoomState( + val confirmation: Confirmation = Confirmation.Hidden, + val progress: Progress = Progress.Hidden, + val error: Error = Error.Hidden, + val eventSink: (LeaveRoomEvent) -> Unit = {}, +) { + sealed interface Confirmation { + object Hidden : Confirmation + data class Generic(val roomId: RoomId) : Confirmation + data class PrivateRoom(val roomId: RoomId) : Confirmation + data class LastUserInRoom(val roomId: RoomId) : Confirmation + } + + sealed interface Progress { + object Hidden : Progress + object Shown : Progress + } + + sealed interface Error { + object Hidden : Error + object Shown : Error + } +} diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt new file mode 100644 index 0000000000..e9b08bcd18 --- /dev/null +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt @@ -0,0 +1,58 @@ +/* + * 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.leaveroom.api + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.core.RoomId + +class LeaveRoomStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + LeaveRoomState( + confirmation = LeaveRoomState.Confirmation.Hidden, + progress = LeaveRoomState.Progress.Hidden, + error = LeaveRoomState.Error.Hidden, + ), + LeaveRoomState( + confirmation = LeaveRoomState.Confirmation.Generic(A_ROOM_ID), + progress = LeaveRoomState.Progress.Hidden, + error = LeaveRoomState.Error.Hidden, + ), + LeaveRoomState( + confirmation = LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID), + progress = LeaveRoomState.Progress.Hidden, + error = LeaveRoomState.Error.Hidden, + ), + LeaveRoomState( + confirmation = LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID), + progress = LeaveRoomState.Progress.Hidden, + error = LeaveRoomState.Error.Hidden, + ), + LeaveRoomState( + confirmation = LeaveRoomState.Confirmation.Hidden, + progress = LeaveRoomState.Progress.Shown, + error = LeaveRoomState.Error.Hidden, + ), + LeaveRoomState( + confirmation = LeaveRoomState.Confirmation.Hidden, + progress = LeaveRoomState.Progress.Hidden, + error = LeaveRoomState.Error.Shown, + ), + ) +} + +private val A_ROOM_ID = RoomId("!aRoomId:aDomain") diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt new file mode 100644 index 0000000000..d1746ca917 --- /dev/null +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt @@ -0,0 +1,131 @@ +/* + * 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.leaveroom.api + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +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.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.R +import io.element.android.libraries.ui.strings.R as StringR + +@Composable +fun LeaveRoomView( + state: LeaveRoomState +) { + LeaveRoomConfirmationDialog(state) + LeaveRoomProgressDialog(state) + LeaveRoomErrorDialog(state) +} + +@Composable +private fun LeaveRoomConfirmationDialog( + state: LeaveRoomState, +) { + when (state.confirmation) { + is LeaveRoomState.Confirmation.Hidden -> {} + is LeaveRoomState.Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog( + text = StringR.string.leave_room_alert_private_subtitle, + roomId = state.confirmation.roomId, + eventSink = state.eventSink, + ) + + is LeaveRoomState.Confirmation.LastUserInRoom -> LeaveRoomConfirmationDialog( + text = StringR.string.leave_room_alert_empty_subtitle, + roomId = state.confirmation.roomId, + eventSink = state.eventSink, + ) + + is LeaveRoomState.Confirmation.Generic -> LeaveRoomConfirmationDialog( + text = StringR.string.leave_room_alert_subtitle, + roomId = state.confirmation.roomId, + eventSink = state.eventSink, + ) + } +} + +@Composable +private fun LeaveRoomConfirmationDialog( + @StringRes text: Int, + roomId: RoomId, + eventSink: (LeaveRoomEvent) -> Unit, +) { + ConfirmationDialog( + content = stringResource(text), + submitText = stringResource(R.string.action_leave), + onSubmitClicked = { eventSink(LeaveRoomEvent.LeaveRoom(roomId)) }, + onDismiss = { eventSink(LeaveRoomEvent.HideConfirmation) }, + ) +} + +@Composable +private fun LeaveRoomProgressDialog( + state: LeaveRoomState, +) { + when (state.progress) { + is LeaveRoomState.Progress.Hidden -> {} + is LeaveRoomState.Progress.Shown -> ProgressDialog( + text = stringResource(StringR.string.common_leaving_room), + ) + } +} + +@Composable +private fun LeaveRoomErrorDialog( + state: LeaveRoomState, +) { + when (state.error) { + is LeaveRoomState.Error.Hidden -> {} + is LeaveRoomState.Error.Shown -> ErrorDialog( + content = stringResource(StringR.string.error_unknown), + onDismiss = { state.eventSink(LeaveRoomEvent.HideError) } + ) + } +} + +@Preview +@Composable +internal fun LeaveRoomViewLightPreview( + @PreviewParameter(LeaveRoomStateProvider::class) state: LeaveRoomState +) = ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +internal fun LeaveRoomViewDarkPreview( + @PreviewParameter(LeaveRoomStateProvider::class) state: LeaveRoomState +) = ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: LeaveRoomState) { + Box( + modifier = Modifier.size(300.dp, 300.dp), + propagateMinConstraints = true, + ) { + LeaveRoomView(state = state) + } +} diff --git a/features/leaveroom/fake/build.gradle.kts b/features/leaveroom/fake/build.gradle.kts new file mode 100644 index 0000000000..19a057d5ba --- /dev/null +++ b/features/leaveroom/fake/build.gradle.kts @@ -0,0 +1,44 @@ +/* + * 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.anvil) +} + +android { + namespace = "io.element.android.features.leaveroom.fake" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + api(projects.features.leaveroom.api) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.coroutines.core) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) +} diff --git a/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFake.kt b/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFake.kt new file mode 100644 index 0000000000..28c12b54ba --- /dev/null +++ b/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFake.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.features.leaveroom.fake + +import androidx.compose.runtime.Composable +import io.element.android.features.leaveroom.api.LeaveRoomEvent +import io.element.android.features.leaveroom.api.LeaveRoomPresenter +import io.element.android.features.leaveroom.api.LeaveRoomState +import javax.inject.Inject + +class LeaveRoomPresenterFake @Inject constructor() : LeaveRoomPresenter { + + val events = mutableListOf() + + private fun handleEvent(event: LeaveRoomEvent) { + events += event + } + + private var state = LeaveRoomState(eventSink = ::handleEvent) + set(value) { + field = value.copy(eventSink = ::handleEvent) + } + + fun givenState(state: LeaveRoomState) { + this.state = state + } + + @Composable + override fun present(): LeaveRoomState = state +} diff --git a/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFakeModule.kt b/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFakeModule.kt new file mode 100644 index 0000000000..b20b88db1c --- /dev/null +++ b/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFakeModule.kt @@ -0,0 +1,30 @@ +/* + * 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.leaveroom.fake + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.leaveroom.api.LeaveRoomPresenter +import io.element.android.libraries.di.SessionScope + +@Module +@ContributesTo(SessionScope::class) +interface LeaveRoomPresenterFakeModule { + @Binds + fun leaveRoomPresenter(leaveRoomPresenter: LeaveRoomPresenterFake): LeaveRoomPresenter +} diff --git a/features/leaveroom/impl/build.gradle.kts b/features/leaveroom/impl/build.gradle.kts new file mode 100644 index 0000000000..8d26ea9271 --- /dev/null +++ b/features/leaveroom/impl/build.gradle.kts @@ -0,0 +1,46 @@ +/* + * 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.anvil) +} + +android { + namespace = "io.element.android.features.leaveroom.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + api(projects.features.leaveroom.api) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.coroutines.core) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) +} diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt new file mode 100644 index 0000000000..4b34e70fb4 --- /dev/null +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt @@ -0,0 +1,127 @@ +/* + * 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.leaveroom.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.features.leaveroom.api.LeaveRoomEvent +import io.element.android.features.leaveroom.api.LeaveRoomPresenter +import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Generic +import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.LastUserInRoom +import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.PrivateRoom +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +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.RoomMembershipObserver +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +class LeaveRoomPresenterImpl @Inject constructor( + private val client: MatrixClient, + private val roomMembershipObserver: RoomMembershipObserver, + private val dispatchers: CoroutineDispatchers, +) : LeaveRoomPresenter { + @Composable + override fun present(): LeaveRoomState { + val scope = rememberCoroutineScope() + val confirmation = remember { mutableStateOf(LeaveRoomState.Confirmation.Hidden) } + val progress = remember { mutableStateOf(LeaveRoomState.Progress.Hidden) } + val error = remember { mutableStateOf(LeaveRoomState.Error.Hidden) } + + return LeaveRoomState( + confirmation = confirmation.value, + progress = progress.value, + error = error.value, + ) { event -> + when (event) { + is LeaveRoomEvent.ShowConfirmation -> scope.launch(dispatchers.io) { + showLeaveRoomAlert( + matrixClient = client, + roomId = event.roomId, + confirmation = confirmation, + ) + } + + is LeaveRoomEvent.HideConfirmation -> confirmation.value = LeaveRoomState.Confirmation.Hidden + is LeaveRoomEvent.LeaveRoom -> scope.launch(dispatchers.io) { + client.leaveRoom( + roomId = event.roomId, + roomMembershipObserver = roomMembershipObserver, + confirmation = confirmation, + progress = progress, + error = error, + ) + } + + is LeaveRoomEvent.HideError -> error.value = LeaveRoomState.Error.Hidden + } + } + } +} + +private suspend fun showLeaveRoomAlert( + matrixClient: MatrixClient, + roomId: RoomId, + confirmation: MutableState, +) { + matrixClient.getRoom(roomId)?.use { room -> + confirmation.value = when { + !room.isPublic -> PrivateRoom(roomId) + (room.memberCount() as? Async.Success)?.state == 1 -> LastUserInRoom(roomId) + else -> Generic(roomId) + } + } +} + +private suspend fun MatrixClient.leaveRoom( + roomId: RoomId, + roomMembershipObserver: RoomMembershipObserver, + confirmation: MutableState, + progress: MutableState, + error: MutableState, +) { + confirmation.value = LeaveRoomState.Confirmation.Hidden + progress.value = LeaveRoomState.Progress.Shown + getRoom(roomId)?.use { room -> + room.leave().onSuccess { + roomMembershipObserver.notifyUserLeftRoom(room.roomId) + }.onFailure { + Timber.e(it, "Error while leaving room ${room.name} - ${room.roomId}") + error.value = LeaveRoomState.Error.Shown + } + } + progress.value = LeaveRoomState.Progress.Hidden +} + +private suspend fun MatrixRoom.memberCount(): Async = membersStateFlow.first().let { membersState -> + when (membersState) { + MatrixRoomMembersState.Unknown -> Async.Uninitialized + is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size) + is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size) + is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.count { it.membership == RoomMembershipState.JOIN }) + } +} diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplModule.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplModule.kt new file mode 100644 index 0000000000..65403adb60 --- /dev/null +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplModule.kt @@ -0,0 +1,30 @@ +/* + * 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.leaveroom.impl + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.leaveroom.api.LeaveRoomPresenter +import io.element.android.libraries.di.SessionScope + +@Module +@ContributesTo(SessionScope::class) +interface LeaveRoomPresenterImplModule { + @Binds + fun leaveRoomPresenter(leaveRoomPresenter: LeaveRoomPresenterImpl): LeaveRoomPresenter +} diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt new file mode 100644 index 0000000000..6dfc23be3f --- /dev/null +++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt @@ -0,0 +1,248 @@ +/* + * 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.leaveroom.impl + +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.leaveroom.api.LeaveRoomEvent +import io.element.android.features.leaveroom.api.LeaveRoomPresenter +import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.UserId +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 +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.test.runTest +import org.junit.Ignore +import org.junit.Test + +class LeaveRoomPresenterImplTest { + + @Test + fun `present - initial state hides all dialogs`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Hidden) + assertThat(initialState.progress).isEqualTo(LeaveRoomState.Progress.Hidden) + assertThat(initialState.error).isEqualTo(LeaveRoomState.Error.Hidden) + } + } + + @Test + fun `present - show generic confirmation`() = runTest { + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom() + ) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) + val confirmationState = awaitItem() + assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Generic(A_ROOM_ID)) + } + } + + @Test + fun `present - show private room confirmation`() = runTest { + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom(isPublic = false), + ) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) + val confirmationState = awaitItem() + assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID)) + } + } + + @Test + fun `present - show last user in room confirmation`() = runTest { + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom().apply { + givenRoomMembersState( + MatrixRoomMembersState.Ready( + listOf( + RoomMember( + userId = UserId(value = "@aUserId:aDomain"), + displayName = null, + avatarUrl = null, + membership = RoomMembershipState.JOIN, + isNameAmbiguous = false, + powerLevel = 0, + normalizedPowerLevel = 0, + isIgnored = false + ) + ) + ) + ) + }, + ) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) + val confirmationState = awaitItem() + assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID)) + } + } + + @Test + fun `present - leaving a room leaves the room`() = runTest { + val roomMembershipObserver = RoomMembershipObserver() + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom(), + ) + }, + roomMembershipObserver = roomMembershipObserver + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + cancelAndIgnoreRemainingEvents() + } + + // Membership observer should receive a 'left room' change + roomMembershipObserver.updates.take(1) + .onEach { update -> assertThat(update.change).isEqualTo(MembershipChange.LEFT) } + .collect() + } + + @Test + fun `present - show error if leave room fails`() = runTest { + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom().apply { + givenLeaveRoomError(RuntimeException("Blimey!")) + }, + ) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + val errorState = awaitItem() + assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown) + } + } + + @Test + @Ignore("TODO(Test the hiding/showing of the progress indicator too)") + fun `present - show progress indicator while leaving a room`() = runTest { + val roomMembershipObserver = RoomMembershipObserver() + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom(), + ) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + val progressState = awaitItem() + assertThat(progressState.progress).isEqualTo(LeaveRoomState.Progress.Shown) + val finalState = awaitItem() + assertThat(finalState.progress).isEqualTo(LeaveRoomState.Progress.Hidden) + } + + // Membership observer should receive a 'left room' change + roomMembershipObserver.updates.take(1) + .onEach { update -> assertThat(update.change).isEqualTo(MembershipChange.LEFT) } + .collect() + } + + @Test + fun `present - hide error hides the error`() = runTest { + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom().apply { + givenLeaveRoomError(RuntimeException("Blimey!")) + }, + ) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + val errorState = awaitItem() + assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown) + errorState.eventSink(LeaveRoomEvent.HideError) + val hiddenErrorState = awaitItem() + assertThat(hiddenErrorState.error).isEqualTo(LeaveRoomState.Error.Hidden) + } + } +} + +private fun createPresenter( + client: MatrixClient = FakeMatrixClient(), + roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), + dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), +): LeaveRoomPresenter = LeaveRoomPresenterImpl( + client = client, + roomMembershipObserver = roomMembershipObserver, + dispatchers = dispatchers, +) diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 0470fc2e65..bcd618eccd 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { api(projects.libraries.usersearch.api) api(projects.services.apperror.api) implementation(libs.coil.compose) + implementation(projects.features.leaveroom.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) @@ -54,6 +55,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.usersearch.test) testImplementation(projects.tests.testutils) + testImplementation(projects.features.leaveroom.fake) ksp(libs.showkase.processor) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt index 3ef87d17e0..b7bb31757e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt @@ -17,7 +17,5 @@ package io.element.android.features.roomdetails.impl sealed interface RoomDetailsEvent { - data class LeaveRoom(val needsConfirmation: Boolean) : RoomDetailsEvent - object ClearLeaveRoomWarning : RoomDetailsEvent - object ClearError : RoomDetailsEvent + object LeaveRoom : RoomDetailsEvent } 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 01562841ea..4e7d88b18e 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 @@ -18,44 +18,33 @@ package io.element.android.features.roomdetails.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.features.leaveroom.api.LeaveRoomEvent +import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter 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.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.ui.room.getDirectRoomMember -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import javax.inject.Inject class RoomDetailsPresenter @Inject constructor( private val room: MatrixRoom, - private val roomMembershipObserver: RoomMembershipObserver, - private val coroutineDispatchers: CoroutineDispatchers, private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory, + private val leaveRoomPresenter: LeaveRoomPresenter, ) : Presenter { @Composable override fun present(): RoomDetailsState { - val coroutineScope = rememberCoroutineScope() - val leaveRoomWarning = remember { - mutableStateOf(null) - } - val error = remember { - mutableStateOf(null) - } + val leaveRoomState = leaveRoomPresenter.present() LaunchedEffect(Unit) { room.updateMembers() } @@ -69,16 +58,8 @@ class RoomDetailsPresenter @Inject constructor( fun handleEvents(event: RoomDetailsEvent) { when (event) { - is RoomDetailsEvent.LeaveRoom -> { - coroutineScope.leaveRoom( - needsConfirmation = event.needsConfirmation, - memberCount = memberCount, - leaveRoomWarning = leaveRoomWarning, - error = error, - ) - } - RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning.value = null - RoomDetailsEvent.ClearError -> error.value = null + is RoomDetailsEvent.LeaveRoom -> + leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(room.roomId)) } } @@ -93,10 +74,9 @@ class RoomDetailsPresenter @Inject constructor( memberCount = memberCount, isEncrypted = room.isEncrypted, canInvite = canInvite, - displayLeaveRoomWarning = leaveRoomWarning.value, - error = error.value, roomType = roomType.value, roomMemberDetailsState = roomMemberDetailsState, + leaveRoomState = leaveRoomState, eventSink = ::handleEvents, ) } @@ -141,27 +121,4 @@ class RoomDetailsPresenter @Inject constructor( } } } - - private fun CoroutineScope.leaveRoom( - needsConfirmation: Boolean, - memberCount: Async, - leaveRoomWarning: MutableState, - error: MutableState, - ) = launch(coroutineDispatchers.io) { - if (needsConfirmation) { - leaveRoomWarning.value = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount) - } else { - room.leave() - .onSuccess { - roomMembershipObserver.notifyUserLeftRoom(room.roomId) - }.onFailure { - error.value = RoomDetailsError.AlertGeneric - } - leaveRoomWarning.value = null - } - } } - - - - 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 90fa48c575..a046548b19 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 @@ -16,6 +16,7 @@ package io.element.android.features.roomdetails.impl +import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.room.RoomMember @@ -28,11 +29,10 @@ data class RoomDetailsState( val roomTopic: String?, val memberCount: Async, val isEncrypted: Boolean, - val displayLeaveRoomWarning: LeaveRoomWarning?, - val error: RoomDetailsError?, val roomType: RoomDetailsType, val roomMemberDetailsState: RoomMemberDetailsState?, val canInvite: Boolean, + val leaveRoomState: LeaveRoomState, val eventSink: (RoomDetailsEvent) -> Unit ) @@ -40,23 +40,3 @@ sealed interface RoomDetailsType { object Room : RoomDetailsType data class Dm(val roomMember: RoomMember) : RoomDetailsType } - -sealed class LeaveRoomWarning { - object Generic : LeaveRoomWarning() - object PrivateRoom : LeaveRoomWarning() - object LastUserInRoom : LeaveRoomWarning() - - companion object { - fun computeLeaveRoomWarning(isPublic: Boolean, memberCount: Async): LeaveRoomWarning { - return when { - !isPublic -> PrivateRoom - (memberCount as? Async.Success)?.state == 1 -> LastUserInRoom - else -> Generic - } - } - } -} - -sealed interface RoomDetailsError { - object AlertGeneric : RoomDetailsError -} 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 08da243487..9cb5b925fa 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 @@ -17,6 +17,7 @@ package io.element.android.features.roomdetails.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.UserId @@ -70,11 +71,10 @@ fun aRoomDetailsState() = RoomDetailsState( "|| MAI iki/Marketing...", memberCount = Async.Success(32), isEncrypted = true, - displayLeaveRoomWarning = null, - error = null, canInvite = false, roomType = RoomDetailsType.Room, roomMemberDetailsState = null, + leaveRoomState = LeaveRoomState(), 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 293f290112..7d90342f65 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 @@ -47,6 +47,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection +import io.element.android.features.leaveroom.api.LeaveRoomView import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection import io.element.android.libraries.architecture.isLoading @@ -56,8 +57,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.MainActionButton -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog -import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -68,7 +67,6 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable @@ -97,6 +95,8 @@ fun RoomDetailsView( .consumeWindowInsets(padding) .verticalScroll(rememberScrollState()) ) { + LeaveRoomView(state = state.leaveRoomState) + when (state.roomType) { RoomDetailsType.Room -> { RoomHeaderSection( @@ -145,23 +145,8 @@ fun RoomDetailsView( } OtherActionsSection(onLeaveRoom = { - state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) + state.eventSink(RoomDetailsEvent.LeaveRoom) }) - - if (state.displayLeaveRoomWarning != null) { - ConfirmLeaveRoomDialog( - leaveRoomWarning = state.displayLeaveRoomWarning, - onConfirmLeave = { state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) }, - onDismiss = { state.eventSink(RoomDetailsEvent.ClearLeaveRoomWarning) } - ) - } - - if (state.error != null) { - ErrorDialog( - content = stringResource(StringR.string.error_unknown), - onDismiss = { state.eventSink(RoomDetailsEvent.ClearError) } - ) - } } } } @@ -260,27 +245,6 @@ internal fun OtherActionsSection(onLeaveRoom: () -> Unit, modifier: Modifier = M } } -@Composable -internal fun ConfirmLeaveRoomDialog( - leaveRoomWarning: LeaveRoomWarning, - onConfirmLeave: () -> Unit, - onDismiss: () -> Unit -) { - val content = stringResource( - when (leaveRoomWarning) { - LeaveRoomWarning.PrivateRoom -> StringR.string.leave_room_alert_private_subtitle - LeaveRoomWarning.LastUserInRoom -> StringR.string.leave_room_alert_empty_subtitle - LeaveRoomWarning.Generic -> StringR.string.leave_room_alert_subtitle - } - ) - ConfirmationDialog( - content = content, - submitText = stringResource(StringR.string.action_leave), - onSubmitClicked = onConfirmLeave, - onDismiss = onDismiss, - ) -} - @LargeHeightPreview @Composable fun RoomDetailsLightPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) = 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 8e97c42936..0c950071ca 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 @@ -20,8 +20,7 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth -import io.element.android.features.roomdetails.impl.LeaveRoomWarning -import io.element.android.features.roomdetails.impl.RoomDetailsEvent +import io.element.android.features.leaveroom.fake.LeaveRoomPresenterFake 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 @@ -34,7 +33,6 @@ 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.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipState -import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -44,9 +42,6 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.take import kotlinx.coroutines.test.runTest import org.junit.Test @@ -62,7 +57,7 @@ class RoomDetailsPresenterTests { return RoomMemberDetailsPresenter(aMatrixClient(), room, roomMemberId) } } - return RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers, roomMemberDetailsPresenterFactory) + return RoomDetailsPresenter(room, roomMemberDetailsPresenterFactory, LeaveRoomPresenterFake()) } @Test @@ -155,103 +150,6 @@ class RoomDetailsPresenterTests { } } - @Test - fun `present - Leave with confirmation on private room shows a specific warning`() = runTest { - val room = aMatrixRoom(isPublic = false).apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) - } - val presenter = aRoomDetailsPresenter(room) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - skipItems(1) - - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) - val confirmationState = awaitItem() - Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.PrivateRoom) - } - } - - @Test - fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest { - val room = aMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(aRoomMember()))) - } - val presenter = aRoomDetailsPresenter(room) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - skipItems(1) - - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) - val confirmationState = awaitItem() - Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.LastUserInRoom) - } - } - - @Test - fun `present - Leave with confirmation shows a generic warning`() = runTest { - val room = aMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) - } - val presenter = aRoomDetailsPresenter(room) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - skipItems(1) - - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) - val confirmationState = awaitItem() - Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.Generic) - } - } - - @Test - fun `present - Leave without confirmation leaves the room`() = runTest { - val room = aMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) - } - val presenter = aRoomDetailsPresenter(room) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - skipItems(1) - - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) - - cancelAndIgnoreRemainingEvents() - } - - // Membership observer should receive a 'left room' change - roomMembershipObserver.updates.take(1) - .onEach { update -> Truth.assertThat(update.change).isEqualTo(MembershipChange.LEFT) } - .collect() - } - - @Test - fun `present - ClearError removes any error present`() = runTest { - val room = aMatrixRoom().apply { - givenLeaveRoomError(Throwable()) - } - val presenter = aRoomDetailsPresenter(room) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - skipItems(1) - - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) - val errorState = awaitItem() - Truth.assertThat(errorState.error).isNotNull() - errorState.eventSink(RoomDetailsEvent.ClearError) - Truth.assertThat(awaitItem().error).isNull() - } - } - @Test fun `present - initial state when user can invite others to room`() = runTest { val room = aMatrixRoom().apply { diff --git a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt index dd4347ba0c..eeefa15c02 100644 --- a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt +++ b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt @@ -36,6 +36,7 @@ interface RoomListEntryPoint : FeatureEntryPoint { fun onSettingsClicked() fun onSessionVerificationClicked() fun onInvitesClicked() + fun onRoomSettingsClicked(roomId: RoomId) } } diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index 9150ff4c95..100ab15437 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(projects.libraries.dateformatter.api) implementation(projects.features.invitelist.api) implementation(projects.features.networkmonitor.api) + implementation(projects.features.leaveroom.api) implementation(libs.accompanist.placeholder) api(projects.features.roomlist.api) ksp(libs.showkase.processor) @@ -66,6 +67,7 @@ dependencies { testImplementation(projects.features.invitelist.test) testImplementation(projects.features.networkmonitor.test) testImplementation(projects.tests.testutils) + testImplementation(projects.features.leaveroom.fake) androidTestImplementation(libs.test.junitext) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt new file mode 100644 index 0000000000..4e172bbbfe --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt @@ -0,0 +1,148 @@ +/* + * 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.roomlist.impl + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +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.unit.dp +import io.element.android.libraries.designsystem.VectorIcons +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.R as StringR + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomListContextMenu( + contextMenu: RoomListState.ContextMenu.Shown, + eventSink: (RoomListEvents) -> Unit, + onRoomSettingsClicked: (roomId: RoomId) -> Unit, +) { + ModalBottomSheet( + onDismissRequest = { eventSink(RoomListEvents.HideContextMenu) }, + ) { + RoomListModalBottomSheetContent( + contextMenu = contextMenu, + onRoomSettingsClicked = { + eventSink(RoomListEvents.HideContextMenu) + onRoomSettingsClicked(it) + }, + onLeaveRoomClicked = { + eventSink(RoomListEvents.HideContextMenu) + eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId)) + } + ) + } +} + +@Composable +private fun RoomListModalBottomSheetContent( + contextMenu: RoomListState.ContextMenu.Shown, + onRoomSettingsClicked: (roomId: RoomId) -> Unit, + onLeaveRoomClicked: (roomId: RoomId) -> Unit, +) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + ListItem( + headlineContent = { + Text( + text = contextMenu.roomName, + fontWeight = FontWeight.Bold, + ) + } + ) + ListItem( + headlineContent = { + Text(text = stringResource(id = StringR.string.common_settings)) + }, + modifier = Modifier.clickable { onRoomSettingsClicked(contextMenu.roomId) }, + leadingContent = { + Icon( + imageVector = Icons.Outlined.Settings, + contentDescription = stringResource(id = StringR.string.common_settings), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + ) + ListItem( + headlineContent = { + Text( + text = stringResource(id = StringR.string.action_leave_room), + color = ElementTheme.colors.textActionCritical, + ) + }, + modifier = Modifier.clickable { onLeaveRoomClicked(contextMenu.roomId) }, + leadingContent = { + Icon( + resourceId = VectorIcons.DoorOpen, + contentDescription = stringResource(id = StringR.string.action_leave_room), + modifier = Modifier.size(20.dp), + tint = ElementTheme.colors.textActionCritical, + ) + } + ) + Spacer(modifier = Modifier.height(32.dp)) + } +} + +// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up. +// see: https://issuetracker.google.com/issues/283843380 +// Remove this preview when the issue is fixed. +@Preview +@Composable +internal fun RoomListModalBottomSheetContentLightPreview() = + ElementPreviewLight { ContentToPreview() } + +// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up. +// see: https://issuetracker.google.com/issues/283843380 +// Remove this preview when the issue is fixed. +@Preview +@Composable +internal fun RoomListModalBottomSheetContentDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + RoomListModalBottomSheetContent( + contextMenu = RoomListState.ContextMenu.Shown( + roomId = RoomId(value = "!aRoom:aDomain"), + roomName = "aRoom" + ), + onRoomSettingsClicked = {}, + onLeaveRoomClicked = {} + ) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt index 684342bee8..e95b5bd60d 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt @@ -16,9 +16,15 @@ package io.element.android.features.roomlist.impl +import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.libraries.matrix.api.core.RoomId + sealed interface RoomListEvents { data class UpdateFilter(val newFilter: String) : RoomListEvents data class UpdateVisibleRange(val range: IntRange) : RoomListEvents object DismissRequestVerificationPrompt : RoomListEvents object ToggleSearchResults : RoomListEvents + data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents + object HideContextMenu : RoomListEvents + data class LeaveRoom(val roomId: RoomId) : RoomListEvents } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt index 7641f015cc..50a7a7bfbe 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt @@ -56,17 +56,22 @@ class RoomListNode @AssistedInject constructor( plugins().forEach { it.onInvitesClicked() } } + private fun onRoomSettingsClicked(roomId: RoomId) { + plugins().forEach { it.onRoomSettingsClicked(roomId) } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() RoomListView( state = state, - modifier = modifier, onRoomClicked = this::onRoomClicked, - onOpenSettings = this::onOpenSettings, + onSettingsClicked = this::onOpenSettings, onCreateRoomClicked = this::onCreateRoomClicked, onVerifyClicked = this::onSessionVerificationClicked, onInvitesClicked = this::onInvitesClicked, + onRoomSettingsClicked = this::onRoomSettingsClicked, + modifier = modifier, ) } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 59faa5afbe..e1f37ae5e6 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -28,6 +28,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.features.leaveroom.api.LeaveRoomEvent +import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders import io.element.android.libraries.architecture.Presenter @@ -62,10 +64,12 @@ class RoomListPresenter @Inject constructor( private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, private val inviteStateDataSource: InviteStateDataSource, + private val leaveRoomPresenter: LeaveRoomPresenter, ) : Presenter { @Composable override fun present(): RoomListState { + val leaveRoomState = leaveRoomPresenter.present() val matrixUser: MutableState = rememberSaveable { mutableStateOf(null) } @@ -97,6 +101,8 @@ class RoomListPresenter @Inject constructor( var displaySearchResults by rememberSaveable { mutableStateOf(false) } + var contextMenu by remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } + fun handleEvents(event: RoomListEvents) { when (event) { is RoomListEvents.UpdateFilter -> filter = event.newFilter @@ -108,6 +114,14 @@ class RoomListPresenter @Inject constructor( } displaySearchResults = !displaySearchResults } + is RoomListEvents.ShowContextMenu -> { + contextMenu = RoomListState.ContextMenu.Shown( + roomId = event.roomListRoomSummary.roomId, + roomName = event.roomListRoomSummary.name + ) + } + is RoomListEvents.HideContextMenu -> contextMenu = RoomListState.ContextMenu.Hidden + is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId)) } } @@ -132,6 +146,8 @@ class RoomListPresenter @Inject constructor( hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, invitesState = inviteStateDataSource.inviteState(), displaySearchResults = displaySearchResults, + contextMenu = contextMenu, + leaveRoomState = leaveRoomState, eventSink = ::handleEvents ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index 0f8de1046d..7905b5bc61 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -17,8 +17,10 @@ package io.element.android.features.roomlist.impl import androidx.compose.runtime.Immutable +import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList @@ -33,8 +35,18 @@ data class RoomListState( val snackbarMessage: SnackbarMessage?, val invitesState: InvitesState, val displaySearchResults: Boolean, - val eventSink: (RoomListEvents) -> Unit -) + val contextMenu: ContextMenu, + val leaveRoomState: LeaveRoomState, + val eventSink: (RoomListEvents) -> Unit, +) { + sealed interface ContextMenu { + object Hidden : ContextMenu + data class Shown( + val roomId: RoomId, + val roomName: String, + ) : ContextMenu + } +} enum class InvitesState { NoInvites, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 8bd6537d52..c5598b5426 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.roomlist.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -39,6 +40,9 @@ open class RoomListStateProvider : PreviewParameterProvider { aRoomListState().copy(invitesState = InvitesState.NewInvites), aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()), aRoomListState().copy(displaySearchResults = true), + aRoomListState().copy(contextMenu = RoomListState.ContextMenu.Shown( + roomId = RoomId("!aRoom:aDomain"), roomName = "A nice room name" + )) ) } @@ -52,6 +56,8 @@ internal fun aRoomListState() = RoomListState( displayVerificationPrompt = false, invitesState = InvitesState.NoInvites, displaySearchResults = false, + contextMenu = RoomListState.ContextMenu.Hidden, + leaveRoomState = LeaveRoomState(), eventSink = {} ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index dc3c7bff7a..49b2c3604c 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -16,7 +16,6 @@ package io.element.android.features.roomlist.impl -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -62,6 +61,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp +import io.element.android.features.leaveroom.api.LeaveRoomView import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import io.element.android.features.roomlist.impl.components.RoomListTopBar import io.element.android.features.roomlist.impl.components.RoomSummaryRow @@ -88,20 +88,38 @@ import io.element.android.libraries.ui.strings.R as StringR @Composable fun RoomListView( state: RoomListState, + onRoomClicked: (RoomId) -> Unit, + onSettingsClicked: () -> Unit, + onVerifyClicked: () -> Unit, + onCreateRoomClicked: () -> Unit, + onInvitesClicked: () -> Unit, + onRoomSettingsClicked: (roomId: RoomId) -> Unit, modifier: Modifier = Modifier, - onRoomClicked: (RoomId) -> Unit = {}, - onOpenSettings: () -> Unit = {}, - onVerifyClicked: () -> Unit = {}, - onCreateRoomClicked: () -> Unit = {}, - onInvitesClicked: () -> Unit = {}, ) { Column(modifier = modifier) { ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) Box { + fun onRoomLongClicked( + roomListRoomSummary: RoomListRoomSummary + ) { + state.eventSink(RoomListEvents.ShowContextMenu(roomListRoomSummary)) + } + + if (state.contextMenu is RoomListState.ContextMenu.Shown) { + RoomListContextMenu( + contextMenu = state.contextMenu, + eventSink = state.eventSink, + onRoomSettingsClicked = onRoomSettingsClicked, + ) + } + + LeaveRoomView(state = state.leaveRoomState) + RoomListContent( state = state, onRoomClicked = onRoomClicked, - onOpenSettings = onOpenSettings, + onRoomLongClicked = { onRoomLongClicked(it) }, + onOpenSettings = onSettingsClicked, onVerifyClicked = onVerifyClicked, onCreateRoomClicked = onCreateRoomClicked, onInvitesClicked = onInvitesClicked, @@ -110,6 +128,7 @@ fun RoomListView( RoomListSearchResultView( state = state, onRoomClicked = onRoomClicked, + onRoomLongClicked = { onRoomLongClicked(it) }, modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) @@ -125,12 +144,12 @@ fun RoomListContent( modifier: Modifier = Modifier, onVerifyClicked: () -> Unit = {}, onRoomClicked: (RoomId) -> Unit = {}, + onRoomLongClicked: (RoomListRoomSummary) -> Unit = {}, onOpenSettings: () -> Unit = {}, onCreateRoomClicked: () -> Unit = {}, onInvitesClicked: () -> Unit = {}, ) { fun onRoomClicked(room: RoomListRoomSummary) { - if (room.roomId == null) return onRoomClicked(room.roomId) } @@ -237,7 +256,11 @@ fun RoomListContent( items = state.roomList, contentType = { room -> room.contentType() }, ) { room -> - RoomSummaryRow(room = room, onClick = ::onRoomClicked) + RoomSummaryRow( + room = room, + onClick = ::onRoomClicked, + onLongClick = onRoomLongClicked, + ) } } } @@ -339,13 +362,25 @@ internal fun RoomListViewDarkPreview(@PreviewParameter(RoomListStateProvider::cl @Composable private fun ContentToPreview(state: RoomListState) { - RoomListView(state) + RoomListView( + state = state, + onRoomClicked = {}, + onSettingsClicked = {}, + onVerifyClicked = {}, + onCreateRoomClicked = {}, + onInvitesClicked = {}, + onRoomSettingsClicked = {} + ) } @Preview @Composable internal fun RoomListSearchResultContentPreview() { ElementPreviewLight { - RoomListSearchResultContent(state = aRoomListState(), onRoomClicked = {}) + RoomListSearchResultContent( + state = aRoomListState(), + onRoomClicked = {}, + onRoomLongClicked = {} + ) } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt index 0709b41ef2..67847977e6 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt @@ -16,8 +16,9 @@ package io.element.android.features.roomlist.impl.components +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -70,17 +71,20 @@ import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator private val minHeight = 72.dp +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun RoomSummaryRow( room: RoomListRoomSummary, + onClick: (RoomListRoomSummary) -> Unit, + onLongClick: (RoomListRoomSummary) -> Unit, modifier: Modifier = Modifier, - onClick: (RoomListRoomSummary) -> Unit = {}, ) { val clickModifier = if (room.isPlaceholder) { modifier } else { - modifier.clickable( + modifier.combinedClickable( onClick = { onClick(room) }, + onLongClick = { onLongClick(room) }, indication = rememberRipple(), interactionSource = remember { MutableInteractionSource() } ) @@ -214,5 +218,9 @@ internal fun RoomSummaryRowDarkPreview(@PreviewParameter(RoomListRoomSummaryProv @Composable private fun ContentToPreview(data: RoomListRoomSummary) { - RoomSummaryRow(data) + RoomSummaryRow( + room = data, + onClick = {}, + onLongClick = {} + ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt index d938cce04f..734d1ce9e1 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt @@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.core.RoomId @Immutable data class RoomListRoomSummary( val id: String, - val roomId: RoomId?, + val roomId: RoomId, val name: String = "", val hasUnread: Boolean = false, val timestamp: String? = null, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryPlaceholders.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryPlaceholders.kt index d6de1544cb..47e3a8ca28 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryPlaceholders.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryPlaceholders.kt @@ -17,13 +17,14 @@ package io.element.android.features.roomlist.impl.model import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.matrix.api.core.RoomId object RoomListRoomSummaryPlaceholders { fun create(id: String): RoomListRoomSummary { return RoomListRoomSummary( id = id, - roomId = null, + roomId = RoomId("!aRoom:domain"), isPlaceholder = true, name = "Short name", timestamp = "hh:mm", diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt index bdd4858ce3..f70c46c18b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt @@ -70,6 +70,7 @@ import io.element.android.libraries.ui.strings.R internal fun RoomListSearchResultView( state: RoomListState, onRoomClicked: (RoomId) -> Unit, + onRoomLongClicked: (RoomListRoomSummary) -> Unit, modifier: Modifier = Modifier, ) { AnimatedVisibility( @@ -85,7 +86,11 @@ internal fun RoomListSearchResultView( }) ) { if (state.displaySearchResults) { - RoomListSearchResultContent(state = state, onRoomClicked = onRoomClicked) + RoomListSearchResultContent( + state = state, + onRoomClicked = onRoomClicked, + onRoomLongClicked = onRoomLongClicked, + ) } } } @@ -96,6 +101,7 @@ internal fun RoomListSearchResultView( internal fun RoomListSearchResultContent( state: RoomListState, onRoomClicked: (RoomId) -> Unit, + onRoomLongClicked: (RoomListRoomSummary) -> Unit, modifier: Modifier = Modifier, ) { val borderColor = MaterialTheme.colorScheme.tertiary @@ -104,7 +110,6 @@ internal fun RoomListSearchResultContent( state.eventSink(RoomListEvents.ToggleSearchResults) } fun onRoomClicked(room: RoomListRoomSummary) { - if (room.roomId == null) return onRoomClicked(room.roomId) } Scaffold( @@ -197,7 +202,11 @@ internal fun RoomListSearchResultContent( items = state.filteredRoomList, contentType = { room -> room.contentType() }, ) { room -> - RoomSummaryRow(room = room, onClick = ::onRoomClicked) + RoomSummaryRow( + room = room, + onClick = ::onRoomClicked, + onLongClick = onRoomLongClicked, + ) } } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index 104c419f74..72996727dd 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -20,8 +20,11 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth +import io.element.android.features.leaveroom.api.LeaveRoomEvent +import io.element.android.features.leaveroom.fake.LeaveRoomPresenterFake import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -54,6 +57,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -82,6 +86,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -103,6 +108,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -132,6 +138,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -164,6 +171,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -202,6 +210,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -253,6 +262,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -278,6 +288,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(inviteStateFlow), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -297,6 +308,89 @@ class RoomListPresenterTests { } } + @Test + fun `present - show context menu`() = runTest { + val presenter = RoomListPresenter( + FakeMatrixClient(A_SESSION_ID), + createDateFormatter(), + FakeRoomLastMessageFormatter(), + FakeSessionVerificationService(), + FakeNetworkMonitor(), + SnackbarDispatcher(), + FakeInviteDataSource(), + LeaveRoomPresenterFake(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + + val initialState = awaitItem() + val summary = aRoomListRoomSummary() + initialState.eventSink(RoomListEvents.ShowContextMenu(summary)) + + val shownState = awaitItem() + Truth.assertThat(shownState.contextMenu) + .isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name)) + } + } + + @Test + fun `present - hide context menu`() = runTest { + val presenter = RoomListPresenter( + FakeMatrixClient(A_SESSION_ID), + createDateFormatter(), + FakeRoomLastMessageFormatter(), + FakeSessionVerificationService(), + FakeNetworkMonitor(), + SnackbarDispatcher(), + FakeInviteDataSource(), + LeaveRoomPresenterFake(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + + val initialState = awaitItem() + val summary = aRoomListRoomSummary() + initialState.eventSink(RoomListEvents.ShowContextMenu(summary)) + + val shownState = awaitItem() + Truth.assertThat(shownState.contextMenu) + .isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name)) + shownState.eventSink(RoomListEvents.HideContextMenu) + + val hiddenState = awaitItem() + Truth.assertThat(hiddenState.contextMenu).isEqualTo(RoomListState.ContextMenu.Hidden) + } + } + + @Test + fun `present - leave room calls into leave room presenter`() = runTest { + val leaveRoomPresenter = LeaveRoomPresenterFake() + val presenter = RoomListPresenter( + FakeMatrixClient(A_SESSION_ID), + createDateFormatter(), + FakeRoomLastMessageFormatter(), + FakeSessionVerificationService(), + FakeNetworkMonitor(), + SnackbarDispatcher(), + FakeInviteDataSource(), + leaveRoomPresenter, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + + val initialState = awaitItem() + initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID)) + + Truth.assertThat(leaveRoomPresenter.events).containsExactly(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) + } + } + private fun createDateFormatter(): LastMessageTimestampFormatter { return FakeLastMessageTimestampFormatter().apply { givenFormat(A_FORMATTED_DATE) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt index a11b0b96bb..c2a82f0e21 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt @@ -22,4 +22,5 @@ object VectorIcons { val Delete = R.drawable.ic_baseline_delete_outline_24 val Reply = R.drawable.ic_baseline_reply_24 val Edit = R.drawable.ic_baseline_edit_24 + val DoorOpen = R.drawable.ic_door_open_24 } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt new file mode 100644 index 0000000000..0c98caaa7d --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt @@ -0,0 +1,103 @@ +/* + * 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.libraries.designsystem.theme.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.contentColorFor +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.preview.PreviewGroup + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ModalBottomSheet( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + sheetState: SheetState = rememberModalBottomSheetState(), + shape: Shape = BottomSheetDefaults.ExpandedShape, + containerColor: Color = BottomSheetDefaults.ContainerColor, + contentColor: Color = contentColorFor(containerColor), + tonalElevation: Dp = BottomSheetDefaults.Elevation, + scrimColor: Color = BottomSheetDefaults.ScrimColor, + dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, + content: @Composable ColumnScope.() -> Unit, +) { + androidx.compose.material3.ModalBottomSheet( + onDismissRequest = onDismissRequest, + modifier = modifier, + sheetState = sheetState, + shape = shape, + containerColor = containerColor, + contentColor = contentColor, + tonalElevation = tonalElevation, + scrimColor = scrimColor, + dragHandle = dragHandle, + content = content, + ) +} + +// This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380 +@Preview(group = PreviewGroup.BottomSheets) +@Composable +internal fun ModalBottomSheetLightPreview() = + ElementPreviewLight { ContentToPreview() } + +// This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380 +@Preview(group = PreviewGroup.BottomSheets) +@Composable +internal fun ModalBottomSheetDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ContentToPreview() { + Box( + modifier = Modifier.fillMaxSize(), + ) { + ModalBottomSheet( + onDismissRequest = {}, + sheetState = SheetState( + skipPartiallyExpanded = true, + initialValue = SheetValue.Expanded, + skipHiddenState = true, + ), + ) { + Text( + text = "Sheet Content", + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 20.dp) + .background(color = Color.Green) + ) + } + } +} diff --git a/libraries/designsystem/src/main/res/drawable/ic_door_open_24.xml b/libraries/designsystem/src/main/res/drawable/ic_door_open_24.xml new file mode 100644 index 0000000000..7d2eec40f5 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_door_open_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 83e358ad48..73e09d2314 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(projects.libraries.dateformatter.impl) implementation(projects.features.invitelist.impl) implementation(projects.features.roomlist.impl) + implementation(projects.features.leaveroom.impl) implementation(projects.features.login.impl) implementation(projects.features.networkmonitor.impl) implementation(libs.coroutines.core) diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index aab7d5ca5f..9a5e1a8631 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import io.element.android.features.invitelist.impl.DefaultSeenInvitesStore +import io.element.android.features.leaveroom.impl.LeaveRoomPresenterImpl import io.element.android.features.networkmonitor.impl.NetworkMonitorImpl import io.element.android.features.roomlist.impl.DefaultInviteStateDataSource import io.element.android.features.roomlist.impl.DefaultRoomLastMessageFormatter @@ -33,6 +34,7 @@ import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.datetime.Clock @@ -57,7 +59,8 @@ class RoomListScreen( sessionVerificationService = sessionVerificationService, networkMonitor = NetworkMonitorImpl(context), snackbarDispatcher = SnackbarDispatcher(), - inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers) + inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers), + leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver() ,coroutineDispatchers) ) @Composable @@ -82,8 +85,13 @@ class RoomListScreen( val state = presenter.present() RoomListView( state = state, - modifier = modifier, onRoomClicked = ::onRoomClicked, + onSettingsClicked = {}, + onVerifyClicked = {}, + onCreateRoomClicked = {}, + onInvitesClicked = {}, + onRoomSettingsClicked = {}, + modifier = modifier, ) DisposableEffect(Unit) { diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..83e91d47af --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f20d59b9bb1ea540e7a1f0c1aeefd1439340625429af672bd6cd00f0c3fdc7f6 +size 5150 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4738df50ef --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03ceca5d30354b7736be1bc079053db5ff82d2ac330161c5e07a5d801a24220a +size 20341 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..31fb5c37a5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfe27aff64c8ec1c71b9c41ebd7cef9a59df69275bbb3637cf4a7e0c88bdcb0e +size 32997 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c8e9a36e71 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2148b3ffec6f6e953617b035702299f4bb1b5f7f56eacce907f333076da75bd9 +size 36701 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fa09501b15 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7de61d642b9b98c9f0bb8b015b5c57d9645a1552ebed6f0cd215c051c47f0896 +size 10593 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dbf334a4a6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59529b30fa2cfef483b365ab888e9327b2dcbae4e40a571642cb534d80379ce2 +size 18439 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2a715455c6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47e2b24a91c59154221e16b1c9e80689300c077d3f19b8dba4d539754c630e70 +size 5144 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ecbf7acd67 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e58c6e0ea9e0df37794d4ac2f8e40ff7995454626618fc0183b5900d62dcfec +size 20456 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..424bc9edae --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71ab2aab2b1aba8baa95c2bd844d5b01410bb3d6004f561038919cf6a7a6b6f5 +size 32991 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..af9816dd4e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ceb2bbf886fdd28c8ed2aa755241725de106ae998825e10d3c6608f100987cb1 +size 36673 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..34e8908056 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:165597506f493c5adedc840fc22ac0218517bb8ae1c4a7f1fabe43665d0f154f +size 10740 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_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.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..104e886089 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:470b5e47f6b21819aa751736f24f7afd88a5ce3ac911f9d0b6bfbdf94e2f918b +size 18597 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentDarkPreview_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.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6ac7bc4bf1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:514d015e7edd0c48746439c9e6a929cd24f92c36302d0d746ca71f0482283439 +size 23123 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentLightPreview_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.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3da8979b19 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78f9c2e01fd3d0ef827808265308555542e5d24055714c7a632e8baf1baffc8e +size 21974 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_8,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.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e4b5f95b8d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cdb131c68de1fce5a3319151e39148e9f3a71c7bc3984e89ec0a80abf0f7288 +size 37044 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_8,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.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2aa0933d88 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2a261b30866af95b856ee1e7d6ac2cbe2d638cf80277645f361b043d2f94e60 +size 36658 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_BottomSheets_ModalBottomSheetDarkPreview_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_BottomSheets_ModalBottomSheetDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fca921c50b --- /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_BottomSheets_ModalBottomSheetDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1c1eedbab868e0c2501220293608572850e052f685f7076ec939b3f1a9abf27 +size 4464 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_BottomSheets_ModalBottomSheetLightPreview_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_BottomSheets_ModalBottomSheetLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /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_BottomSheets_ModalBottomSheetLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457