diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index 13e0c3b4ad..1fa53d24ef 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -42,7 +42,7 @@ jobs: name: elementx-debug path: | app/build/outputs/apk/debug/*.apk - - uses: mobile-dev-inc/action-maestro-cloud@v1.3.1 + - uses: mobile-dev-inc/action-maestro-cloud@v1.3.2 with: api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} app-file: app/build/outputs/apk/debug/app-universal-debug.apk diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml new file mode 100644 index 0000000000..5c2110197f --- /dev/null +++ b/.github/workflows/nightlyReports.yml @@ -0,0 +1,43 @@ +name: Nightly reports + +on: + workflow_dispatch: + schedule: + # Every nights at 5 + - cron: "0 5 * * *" + +# Enrich gradle.properties for CI/CD +env: + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false + CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 + +jobs: + nightlyReports: + name: Create kover report artifact and upload sonar result. + runs-on: ubuntu-latest + if: ${{ github.repository == 'vector-im/element-x-android' }} + steps: + - uses: actions/checkout@v3 + - name: Use JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '17' + + - name: βš™οΈ Run unit & screenshot tests, generate kover report + run: ./gradlew koverMergedReport $CI_GRADLE_ARG_PROPERTIES -Pci-build=true + + - name: βœ… Upload kover report + if: always() + uses: actions/upload-artifact@v3 + with: + name: kover-results + path: | + **/build/reports/kover/merged + + - name: πŸ”Š Publish results to Sonar + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }} + if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }} + run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 391c6c6b21..7b6e5eaf7d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -68,13 +68,9 @@ jobs: path: tests/uitests/out/failures/ retention-days: 5 - - name: βœ… Upload kover report + - name: βœ… Upload kover report (disabled) if: always() - uses: actions/upload-artifact@v3 - with: - name: kover-results - path: | - **/build/reports/kover/merged + run: echo "This is now done only once a day, see nightlyReports.yml" - name: 🚫 Upload test results on error if: failure() @@ -85,12 +81,8 @@ jobs: **/out/failures/ **/build/reports/tests/*UnitTest/ - - name: πŸ”Š Publish results to Sonar - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }} - if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }} - run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES + - name: πŸ”Š Publish results to Sonar (disabled) + run: echo "This is now done only once a day, see nightlyReports.yml" # https://github.com/codecov/codecov-action - name: β˜‚οΈ Upload coverage reports to codecov diff --git a/anvilcodegen/build.gradle.kts b/anvilcodegen/build.gradle.kts index dd6033c7e4..6712bddee0 100644 --- a/anvilcodegen/build.gradle.kts +++ b/anvilcodegen/build.gradle.kts @@ -25,6 +25,6 @@ dependencies { implementation(libs.anvil.compiler.utils) implementation("com.squareup:kotlinpoet:1.13.2") implementation(libs.dagger) - compileOnly("com.google.auto.service:auto-service-annotations:1.0.1") - kapt("com.google.auto.service:auto-service:1.0.1") + compileOnly("com.google.auto.service:auto-service-annotations:1.1.0") + kapt("com.google.auto.service:auto-service:1.1.0") } diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 9ba856aa4c..5b1c4b1ced 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(projects.tests.uitests) implementation(libs.coil) + implementation(projects.services.apperror.impl) implementation(projects.services.appnavstate.api) testImplementation(libs.test.junit) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt index 9ac4d34b98..cffc4cf35c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt @@ -17,24 +17,30 @@ package io.element.android.appnav.root import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter import io.element.android.features.rageshake.api.detection.RageshakeDetectionPresenter import io.element.android.libraries.architecture.Presenter +import io.element.android.services.apperror.api.AppErrorStateService import javax.inject.Inject class RootPresenter @Inject constructor( private val crashDetectionPresenter: CrashDetectionPresenter, private val rageshakeDetectionPresenter: RageshakeDetectionPresenter, + private val appErrorStateService: AppErrorStateService, ) : Presenter { @Composable override fun present(): RootState { val rageshakeDetectionState = rageshakeDetectionPresenter.present() val crashDetectionState = crashDetectionPresenter.present() + val appErrorState by appErrorStateService.appErrorStateFlow.collectAsState() return RootState( rageshakeDetectionState = rageshakeDetectionState, crashDetectionState = crashDetectionState, + errorState = appErrorState, ) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt index 8389e1f144..704adb5df3 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt @@ -19,9 +19,11 @@ package io.element.android.appnav.root import androidx.compose.runtime.Immutable import io.element.android.features.rageshake.api.crash.CrashDetectionState import io.element.android.features.rageshake.api.detection.RageshakeDetectionState +import io.element.android.services.apperror.api.AppErrorState @Immutable data class RootState( val rageshakeDetectionState: RageshakeDetectionState, val crashDetectionState: CrashDetectionState, + val errorState: AppErrorState, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt index 645fbccd0d..c8b5413e60 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt @@ -19,6 +19,8 @@ package io.element.android.appnav.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.rageshake.api.crash.aCrashDetectionState import io.element.android.features.rageshake.api.detection.aRageshakeDetectionState +import io.element.android.services.apperror.api.AppErrorState +import io.element.android.services.apperror.api.aAppErrorState open class RootStateProvider : PreviewParameterProvider { override val values: Sequence @@ -30,6 +32,9 @@ open class RootStateProvider : PreviewParameterProvider { aRootState().copy( rageshakeDetectionState = aRageshakeDetectionState().copy(showDialog = true), crashDetectionState = aCrashDetectionState().copy(crashDetected = false), + ), + aRootState().copy( + errorState = aAppErrorState(), ) ) } @@ -37,4 +42,5 @@ open class RootStateProvider : PreviewParameterProvider { fun aRootState() = RootState( rageshakeDetectionState = aRageshakeDetectionState(), crashDetectionState = aCrashDetectionState(), + errorState = AppErrorState.NoError, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt index fc25ee65b1..a52ee59261 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt @@ -31,6 +31,7 @@ import io.element.android.features.rageshake.api.detection.RageshakeDetectionVie import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.services.apperror.impl.AppErrorView @Composable fun RootView( @@ -60,6 +61,9 @@ fun RootView( state = state.crashDetectionState, onOpenBugReport = ::onOpenBugReport, ) + AppErrorView( + state = state.errorState, + ) } } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt index 7c45620afd..0efa9e7f3b 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt @@ -28,6 +28,9 @@ import io.element.android.features.rageshake.test.crash.FakeCrashDataStore import io.element.android.features.rageshake.test.rageshake.FakeRageShake import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder +import io.element.android.services.apperror.api.AppErrorState +import io.element.android.services.apperror.api.AppErrorStateService +import io.element.android.services.apperror.impl.DefaultAppErrorStateService import kotlinx.coroutines.test.runTest import org.junit.Test @@ -44,7 +47,32 @@ class RootPresenterTest { } } - private fun createPresenter(): RootPresenter { + @Test + fun `present - passes app error state`() = runTest { + val presenter = createPresenter( + appErrorService = DefaultAppErrorStateService().apply { + showError("Bad news", "Something bad happened") + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + + val initialState = awaitItem() + assertThat(initialState.errorState).isInstanceOf(AppErrorState.Error::class.java) + val initialErrorState = initialState.errorState as AppErrorState.Error + assertThat(initialErrorState.title).isEqualTo("Bad news") + assertThat(initialErrorState.body).isEqualTo("Something bad happened") + + initialErrorState.dismiss() + assertThat(awaitItem().errorState).isInstanceOf(AppErrorState.NoError::class.java) + } + } + + private fun createPresenter( + appErrorService: AppErrorStateService = DefaultAppErrorStateService() + ): RootPresenter { val crashDataStore = FakeCrashDataStore() val rageshakeDataStore = FakeRageshakeDataStore() val rageshake = FakeRageShake() @@ -63,6 +91,7 @@ class RootPresenterTest { return RootPresenter( crashDetectionPresenter = crashDetectionPresenter, rageshakeDetectionPresenter = rageshakeDetectionPresenter, + appErrorStateService = appErrorService, ) } } diff --git a/build.gradle.kts b/build.gradle.kts index 3d049e5376..3855f48400 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,7 +59,7 @@ allprojects { config = files("$rootDir/tools/detekt/detekt.yml") } dependencies { - detektPlugins("io.nlopez.compose.rules:detekt:0.1.5") + detektPlugins("io.nlopez.compose.rules:detekt:0.1.6") } // KtLint diff --git a/changelog.d/245.feature b/changelog.d/245.feature new file mode 100644 index 0000000000..204107717a --- /dev/null +++ b/changelog.d/245.feature @@ -0,0 +1 @@ +[Create and join rooms] Add ability to invite users to existing rooms diff --git a/changelog.d/420.doc b/changelog.d/420.doc new file mode 100644 index 0000000000..b29185bcd6 --- /dev/null +++ b/changelog.d/420.doc @@ -0,0 +1 @@ +Update design documentation to add references to Compound diff --git a/changelog.d/437.feature b/changelog.d/437.feature new file mode 100644 index 0000000000..3aa3eb97f0 --- /dev/null +++ b/changelog.d/437.feature @@ -0,0 +1 @@ +Handle "Invite people" action in the start a chat flow diff --git a/docs/design.md b/docs/design.md index c46ee0e84d..58723eb28d 100644 --- a/docs/design.md +++ b/docs/design.md @@ -8,11 +8,12 @@ * [Text](#text) * [Dimension, position and margin](#dimension-position-and-margin) * [Icons](#icons) - * [Export drawable from Figma](#export-drawable-from-figma) - * [Import in Android Studio](#import-in-android-studio) + * [Custom icons](#custom-icons) + * [Export drawable from Figma](#export-drawable-from-figma) + * [Import in Android Studio](#import-in-android-studio) * [Images](#images) * [Figma links](#figma-links) - * [Coumpound](#coumpound) + * [Compound](#compound) * [Login](#login) * [Login v2](#login-v2) * [Room list](#room-list) @@ -31,6 +32,7 @@ ## Introduction Design at element.io is done using Figma - https://www.figma.com +You will find guidance to build using interface on the [Compound documentation – Element's design system](https://compound.element.io) ## How to import from Figma to the Element Android project @@ -41,6 +43,9 @@ Integration should be done using the Android development best practice, and shou Element Android already contains all the colors which can be used by the designer, in the module `ui-style`. Some of them depend on the theme, so ensure to use theme attributes and not colors directly. +A comprehensive [color definition documentation](https://compound.element.io/?path=/docs/tokens-color-palettes--docs) is available in Compound. + + ### Text - click on a text on Figma @@ -56,7 +61,16 @@ Some of them depend on the theme, so ensure to use theme attributes and not colo ### Icons -#### Export drawable from Figma +Most icons should be available as part of the [Compound icon library](https://compound.element.io/?path=/docs/tokens-icons--docs) + +All drawable are auto-generated as part of the design tokens library. You can find +all assets in [`vector-im/compound-design-tokens#assets/android`](https://github.com/vector-im/compound-design-tokens/tree/develop/assets/android) + +If you are missing an icon, follow to [contribution guidelines for icons](https://www.figma.com/file/gkNXqPoiJhEv2wt0EJpew4/Compound-Icons?type=design&node-id=178-3119&t=j2uSJD9xPXJn5aRM-0) + +#### Custom icons + +##### Export drawable from Figma - click on the element to export - ensure that the correct layer is selected. Sometimes the parent layer has to be selected on the left panel @@ -68,7 +82,7 @@ Some of them depend on the theme, so ensure to use theme attributes and not colo It's also possible for any icon to go to the main component by right-clicking on the icon. -#### Import in Android Studio +##### Import in Android Studio - right click on the drawable folder where the drawable will be created - click on "New"/"Vector Asset" @@ -97,11 +111,15 @@ Main entry point: https://www.figma.com/files/project/5612863/Element?fuid=77937 Note: all the Figma links are not publicly available. -### Coumpound +### Compound -Coumpound contains the theme of the application, with all the components, in Light and Dark theme: palette (colors), typography, iconography, etc. +Compound is Element's design system where you'll find styles and documentation +regarding user interfaces. -https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound +- Documentation: [https://compound.element.io](https://compound.element.io) +- [Compound Android – Figma document](https://www.figma.com/file/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components) +- [Compound Styles - Figma document](https://www.figma.com/file/PpKepmHKGikp33Ql7iivbn/Compound-Styles?type=design) +- [Compound Icons - Figma document](https://www.figma.com/file/gkNXqPoiJhEv2wt0EJpew4/Compound-Icons) ### Login diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index feab0f16e7..65c43e8fc3 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.elementresources) implementation(projects.libraries.uiStrings) + implementation(projects.libraries.androidutils) implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.mediaupload.api) implementation(libs.coil.compose) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index 8bb82d3d2d..c64c1efc75 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.createroom.impl.R import io.element.android.features.createroom.impl.components.UserListView +import io.element.android.features.createroom.impl.userlist.UserListEvents import io.element.android.features.createroom.impl.userlist.UserListState import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -54,13 +55,17 @@ fun AddPeopleView( Scaffold( modifier = modifier, topBar = { - if (!state.isSearchActive) { - AddPeopleViewTopBar( - hasSelectedUsers = state.selectedUsers.isNotEmpty(), - onBackPressed = onBackPressed, - onNextPressed = onNextPressed, - ) - } + AddPeopleViewTopBar( + hasSelectedUsers = state.selectedUsers.isNotEmpty(), + onBackPressed = { + if (state.isSearchActive) { + state.eventSink(UserListEvents.OnSearchActiveChanged(false)) + } else { + onBackPressed() + } + }, + onNextPressed = onNextPressed, + ) } ) { padding -> Column( @@ -73,6 +78,7 @@ fun AddPeopleView( modifier = Modifier .fillMaxWidth(), state = state, + showBackButton = false, ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt index 33d308941f..be87e1cf51 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt @@ -39,6 +39,7 @@ fun SearchUserBar( active: Boolean, isMultiSelectionEnabled: Boolean, modifier: Modifier = Modifier, + showBackButton: Boolean = true, placeHolderTitle: String = stringResource(R.string.common_search_for_someone), onActiveChanged: (Boolean) -> Unit = {}, onTextChanged: (String) -> Unit = {}, @@ -52,6 +53,7 @@ fun SearchUserBar( onActiveChange = onActiveChanged, modifier = modifier, placeHolderTitle = placeHolderTitle, + showBackButton = showBackButton, contentPrefix = { if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) { SelectedUsersList( diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt index 299dc59abf..7d543261fc 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt @@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.ui.components.SelectedUsersList fun UserListView( state: UserListState, modifier: Modifier = Modifier, + showBackButton: Boolean = true, onUserSelected: (MatrixUser) -> Unit = {}, onUserDeselected: (MatrixUser) -> Unit = {}, ) { @@ -49,6 +50,7 @@ fun UserListView( selectedUsers = state.selectedUsers, active = state.isSearchActive, isMultiSelectionEnabled = state.isMultiSelectionEnabled, + showBackButton = showBackButton, onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) }, onTextChanged = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) }, onUserSelected = { diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt index 33691009ed..7d8211aea5 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt @@ -19,7 +19,6 @@ package io.element.android.features.createroom.impl.root import io.element.android.libraries.matrix.api.user.MatrixUser sealed interface CreateRoomRootEvents { - object InvitePeople : CreateRoomRootEvents data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents object CancelStartDM : CreateRoomRootEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt index 596255dc15..6b5ac667c5 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt @@ -16,8 +16,10 @@ package io.element.android.features.createroom.impl.root +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin @@ -25,14 +27,22 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.androidutils.system.startSharePlainTextIntent +import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder +import io.element.android.libraries.ui.strings.R +import timber.log.Timber @ContributesNode(SessionScope::class) class CreateRoomRootNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: CreateRoomRootPresenter, + private val matrixClient: MatrixClient, + private val buildMeta: BuildMeta, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { @@ -53,12 +63,31 @@ class CreateRoomRootNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { val state = presenter.present() + val context = LocalContext.current CreateRoomRootView( state = state, modifier = modifier, onClosePressed = this::navigateUp, onNewRoomClicked = callback::onCreateNewRoom, onOpenDM = callback::onStartChatSuccess, + onInviteFriendsClicked = { invitePeople(context) } ) } + + private fun invitePeople(context: Context) { + val permalinkResult = PermalinkBuilder.permalinkForUser(matrixClient.sessionId) + permalinkResult.onSuccess { permalink -> + val appName = buildMeta.applicationName + startSharePlainTextIntent( + context = context, + activityResultLauncher = null, + chooserTitle = context.getString(R.string.action_invite_friends), + text = context.getString(R.string.invite_friends_text, appName, permalink), + extraTitle = context.getString(R.string.invite_friends_rich_title, appName), + noActivityFoundMessage = context.getString(io.element.android.libraries.androidutils.R.string.error_no_compatible_app_found) + ) + }.onFailure { + Timber.e(it) + } + } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 602f0a575b..7b5c6b7cf1 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -75,7 +75,6 @@ class CreateRoomRootPresenter @Inject constructor( when (event) { is CreateRoomRootEvents.StartDM -> startDm(event.matrixUser) CreateRoomRootEvents.CancelStartDM -> startDmAction.value = Async.Uninitialized - CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 205533ce00..21abc6546d 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -64,6 +64,7 @@ fun CreateRoomRootView( onClosePressed: () -> Unit = {}, onNewRoomClicked: () -> Unit = {}, onOpenDM: (RoomId) -> Unit = {}, + onInviteFriendsClicked: () -> Unit = {}, ) { if (state.startDmAction is Async.Success) { LaunchedEffect(state.startDmAction) { @@ -96,7 +97,7 @@ fun CreateRoomRootView( if (!state.userListState.isSearchActive) { CreateRoomActionButtonsList( onNewRoomClicked = onNewRoomClicked, - onInvitePeopleClicked = { state.eventSink(CreateRoomRootEvents.InvitePeople) }, + onInvitePeopleClicked = onInviteFriendsClicked, ) } } diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index 517afc9fe9..8d9819eeae 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -62,16 +62,6 @@ class CreateRoomRootPresenterTests { } } - @Test - fun `present - trigger action buttons`() = runTest { - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink(CreateRoomRootEvents.InvitePeople) // Not implemented yet - } - } - @Test fun `present - trigger create DM action`() = runTest { moleculeFlow(RecompositionClock.Immediate) { diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 05181c3f67..0470fc2e65 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(projects.libraries.androidutils) api(projects.features.roomdetails.api) api(projects.libraries.usersearch.api) + api(projects.services.apperror.api) implementation(libs.coil.compose) testImplementation(libs.test.junit) @@ -51,6 +52,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.usersearch.test) testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 8c7d9cd375..97fb3311cb 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -28,6 +28,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode import io.element.android.features.roomdetails.impl.members.RoomMemberListNode import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode import io.element.android.libraries.architecture.BackstackNode @@ -57,6 +58,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize object RoomMemberList : NavTarget + @Parcelize + object InviteMembers : NavTarget + @Parcelize data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget } @@ -68,6 +72,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( override fun openRoomMemberList() { backstack.push(NavTarget.RoomMemberList) } + + override fun openInviteMembers() { + backstack.push(NavTarget.InviteMembers) + } } createNode(buildContext, listOf(roomDetailsCallback)) } @@ -76,9 +84,16 @@ class RoomDetailsFlowNode @AssistedInject constructor( override fun openRoomMemberDetails(roomMemberId: UserId) { backstack.push(NavTarget.RoomMemberDetails(roomMemberId)) } + + override fun openInviteMembers() { + backstack.push(NavTarget.InviteMembers) + } } createNode(buildContext, listOf(roomMemberListCallback)) } + NavTarget.InviteMembers -> { + createNode(buildContext) + } is NavTarget.RoomMemberDetails -> { createNode(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMemberId))) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 37110e5192..8fe4f774d7 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -45,6 +45,7 @@ class RoomDetailsNode @AssistedInject constructor( interface Callback : Plugin { fun openRoomMemberList() + fun openInviteMembers() } private val callbacks = plugins() @@ -53,6 +54,10 @@ class RoomDetailsNode @AssistedInject constructor( callbacks.forEach { it.openRoomMemberList() } } + private fun invitePeople() { + callbacks.forEach { it.openInviteMembers() } + } + private fun onShareRoom(context: Context) { val alias = room.alias ?: room.alternativeAliases.firstOrNull() val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) } @@ -105,6 +110,7 @@ class RoomDetailsNode @AssistedInject constructor( onShareRoom = ::onShareRoom, onShareMember = ::onShareMember, openRoomMemberList = ::openRoomMemberList, + invitePeople = ::invitePeople, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 81b2e1e383..01562841ea 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -62,6 +62,7 @@ class RoomDetailsPresenter @Inject constructor( val membersState by room.membersStateFlow.collectAsState() val memberCount by getMemberCount(membersState) + val canInvite by getCanInvite(membersState) val dmMember by room.getDirectRoomMember(membersState) val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) val roomType = getRoomType(dmMember) @@ -76,7 +77,7 @@ class RoomDetailsPresenter @Inject constructor( error = error, ) } - is RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning.value = null + RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning.value = null RoomDetailsEvent.ClearError -> error.value = null } } @@ -91,6 +92,7 @@ class RoomDetailsPresenter @Inject constructor( roomTopic = room.topic, memberCount = memberCount, isEncrypted = room.isEncrypted, + canInvite = canInvite, displayLeaveRoomWarning = leaveRoomWarning.value, error = error.value, roomType = roomType.value, @@ -117,6 +119,15 @@ class RoomDetailsPresenter @Inject constructor( } } + @Composable + private fun getCanInvite(membersState: MatrixRoomMembersState): State { + val canInvite = remember(membersState) { mutableStateOf(false) } + LaunchedEffect(membersState) { + canInvite.value = room.canInvite().getOrElse { false } + } + return canInvite + } + @Composable private fun getMemberCount(membersState: MatrixRoomMembersState): State> { return remember(membersState) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index 173ba66ed0..90fa48c575 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -32,6 +32,7 @@ data class RoomDetailsState( val error: RoomDetailsError?, val roomType: RoomDetailsType, val roomMemberDetailsState: RoomMemberDetailsState?, + val canInvite: Boolean, val eventSink: (RoomDetailsEvent) -> Unit ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index b58ae355dd..08da243487 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -33,6 +33,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider aRoomDetailsState().copy(memberCount = Async.Failure(Throwable())), aDmRoomDetailsState().copy(roomName = "Daniel"), aDmRoomDetailsState(isDmMemberIgnored = true).copy(roomName = "Daniel"), + aRoomDetailsState().copy(canInvite = true), // Add other state here ) } @@ -71,6 +72,7 @@ fun aRoomDetailsState() = RoomDetailsState( isEncrypted = true, displayLeaveRoomWarning = null, error = null, + canInvite = false, roomType = RoomDetailsType.Room, roomMemberDetailsState = null, eventSink = {} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index c09a80cf1b..293f290112 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -78,6 +78,7 @@ fun RoomDetailsView( onShareRoom: () -> Unit, onShareMember: (RoomMember) -> Unit, openRoomMemberList: () -> Unit, + invitePeople: () -> Unit, modifier: Modifier = Modifier, ) { @@ -127,7 +128,9 @@ fun RoomDetailsView( MembersSection( memberCount = memberCount, isLoading = state.memberCount.isLoading(), - openRoomMemberList = openRoomMemberList + showInvite = state.canInvite, + openRoomMemberList = openRoomMemberList, + invitePeople = invitePeople, ) } @@ -211,8 +214,10 @@ internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) { internal fun MembersSection( memberCount: Int?, isLoading: Boolean, + showInvite: Boolean, + invitePeople: () -> Unit, + openRoomMemberList: () -> Unit, modifier: Modifier = Modifier, - openRoomMemberList: () -> Unit ) { PreferenceCategory(modifier = modifier) { PreferenceText( @@ -222,10 +227,13 @@ internal fun MembersSection( onClick = openRoomMemberList, loadingCurrentValue = isLoading, ) - PreferenceText( - title = stringResource(R.string.screen_room_details_invite_people_title), - icon = Icons.Outlined.PersonAddAlt, - ) + if (showInvite) { + PreferenceText( + title = stringResource(R.string.screen_room_details_invite_people_title), + icon = Icons.Outlined.PersonAddAlt, + onClick = invitePeople, + ) + } } } @@ -291,5 +299,6 @@ private fun ContentToPreview(state: RoomDetailsState) { onShareRoom = {}, onShareMember = {}, openRoomMemberList = {}, + invitePeople = {}, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersEvents.kt new file mode 100644 index 0000000000..80be60fb8c --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersEvents.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.invite + +import io.element.android.libraries.matrix.api.user.MatrixUser + +sealed interface RoomInviteMembersEvents { + data class ToggleUser(val user: MatrixUser) : RoomInviteMembersEvents + data class UpdateSearchQuery(val query: String) : RoomInviteMembersEvents + data class OnSearchActiveChanged(val active: Boolean) : RoomInviteMembersEvents +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt new file mode 100644 index 0000000000..22e91ce5a7 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.invite + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.services.apperror.api.AppErrorStateService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import io.element.android.libraries.ui.strings.R as StringR + +@ContributesNode(RoomScope::class) +class RoomInviteMembersNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + coroutineDispatchers: CoroutineDispatchers, + private val room: MatrixRoom, + private val presenter: RoomInviteMembersPresenter, + private val appErrorStateService: AppErrorStateService, +) : Node(buildContext, plugins = plugins) { + + private val coroutineScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.io) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val context = LocalContext.current.applicationContext + + RoomInviteMembersView( + state = state, + modifier = modifier, + onBackPressed = { navigateUp() }, + onSendPressed = { users -> + navigateUp() + + coroutineScope.launch { + val anyInviteFailed = users + .map { room.inviteUserById(it.userId) } + .any { it.isFailure } + + if (anyInviteFailed) { + appErrorStateService.showError( + title = context.getString(StringR.string.common_unable_to_invite_title), + body = context.getString(StringR.string.common_unable_to_invite_message), + ) + } + + room.updateMembers() + } + } + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt new file mode 100644 index 0000000000..07b3bb1708 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.invite + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.execute +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.usersearch.api.UserRepository +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class RoomInviteMembersPresenter @Inject constructor( + private val userRepository: UserRepository, + private val roomMemberListDataSource: RoomMemberListDataSource, + private val coroutineDispatchers: CoroutineDispatchers, +) : Presenter { + + @Composable + override fun present(): RoomInviteMembersState { + val roomMembers = remember { mutableStateOf>>(Async.Loading()) } + val selectedUsers = remember { mutableStateOf>(persistentListOf()) } + val searchResults = remember { mutableStateOf>>(SearchBarResultState.NotSearching()) } + var searchQuery by rememberSaveable { mutableStateOf("") } + var searchActive by rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(Unit) { + fetchMembers(roomMembers) + } + + LaunchedEffect(searchQuery, roomMembers) { + performSearch(searchResults, roomMembers, selectedUsers, searchQuery) + } + + return RoomInviteMembersState( + canInvite = selectedUsers.value.isNotEmpty(), + selectedUsers = selectedUsers.value, + searchQuery = searchQuery, + isSearchActive = searchActive, + searchResults = searchResults.value, + eventSink = { + when (it) { + is RoomInviteMembersEvents.OnSearchActiveChanged -> { + searchActive = it.active + searchQuery = "" + } + + is RoomInviteMembersEvents.UpdateSearchQuery -> { + searchQuery = it.query + } + + is RoomInviteMembersEvents.ToggleUser -> { + selectedUsers.toggleUser(it.user) + searchResults.toggleUser(it.user) + } + } + } + ) + } + + @JvmName("toggleUserInSelectedUsers") + private fun MutableState>.toggleUser(user: MatrixUser) { + value = if (value.contains(user)) { + value.filterNot { it == user } + } else { + (value + user) + }.toImmutableList() + } + + @JvmName("toggleUserInSearchResults") + private fun MutableState>>.toggleUser(user: MatrixUser) { + val existingResults = value + if (existingResults is SearchBarResultState.Results) { + value = SearchBarResultState.Results( + existingResults.results.map { iu -> + if (iu.matrixUser == user) { + iu.copy(isSelected = !iu.isSelected) + } else { + iu + } + }.toImmutableList() + ) + } + } + + private suspend fun performSearch( + searchResults: MutableState>>, + roomMembers: MutableState>>, + selectedUsers: MutableState>, + searchQuery: String, + ) = withContext(coroutineDispatchers.io) { + searchResults.value = SearchBarResultState.NotSearching() + + val joinedMembers = roomMembers.value.dataOrNull().orEmpty() + + userRepository.search(searchQuery).collect { + searchResults.value = when { + it.isEmpty() -> SearchBarResultState.NoResults() + else -> SearchBarResultState.Results(it.map { user -> + val existingMembership = joinedMembers.firstOrNull { j -> j.userId == user.userId }?.membership + val isJoined = existingMembership == RoomMembershipState.JOIN + val isInvited = existingMembership == RoomMembershipState.INVITE + InvitableUser( + matrixUser = user, + isSelected = selectedUsers.value.contains(user) || isJoined || isInvited, + isAlreadyJoined = isJoined, + isAlreadyInvited = isInvited, + ) + }.toImmutableList()) + } + } + } + + private suspend fun fetchMembers(roomMembers: MutableState>>) { + suspend { + withContext(coroutineDispatchers.io) { + roomMemberListDataSource.search("").toImmutableList() + } + }.execute(roomMembers) + } +} + diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt new file mode 100644 index 0000000000..b96cc30935 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.invite + +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +data class RoomInviteMembersState( + val canInvite: Boolean = false, + val searchQuery: String = "", + val searchResults: SearchBarResultState> = SearchBarResultState.NotSearching(), + val selectedUsers: ImmutableList = persistentListOf(), + val isSearchActive: Boolean = false, + val eventSink: (RoomInviteMembersEvents) -> Unit = {}, +) + +data class InvitableUser( + val matrixUser: MatrixUser, + val isSelected: Boolean = false, + val isAlreadyJoined: Boolean = false, + val isAlreadyInvited: Boolean = false, +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt new file mode 100644 index 0000000000..137d47b660 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.invite + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class RoomInviteMembersStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + RoomInviteMembersState(), + RoomInviteMembersState(canInvite = true, selectedUsers = aMatrixUserList().toImmutableList()), + RoomInviteMembersState(isSearchActive = true, searchQuery = "some query"), + RoomInviteMembersState(isSearchActive = true, searchQuery = "some query", selectedUsers = aMatrixUserList().toImmutableList()), + RoomInviteMembersState(isSearchActive = true, searchQuery = "some query", searchResults = SearchBarResultState.NoResults()), + RoomInviteMembersState( + isSearchActive = true, + canInvite = true, + searchQuery = "some query", + selectedUsers = persistentListOf( + aMatrixUser("@carol:server.org", "Carol") + ), + searchResults = SearchBarResultState.Results( + persistentListOf( + InvitableUser(aMatrixUser("@alice:server.org")), + InvitableUser(aMatrixUser("@bob:server.org", "Bob")), + InvitableUser(aMatrixUser("@carol:server.org", "Carol"), isSelected = true), + InvitableUser(aMatrixUser("@eve:server.org", "Eve"), isSelected = true, isAlreadyJoined = true), + InvitableUser(aMatrixUser("@justin:server.org", "Justin"), isSelected = true, isAlreadyInvited = true), + ) + ) + ), + ) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt new file mode 100644 index 0000000000..313ad25346 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.invite + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.features.roomdetails.impl.R +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.SearchBar +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.CheckableUserRow +import io.element.android.libraries.matrix.ui.components.SelectedUsersList +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.matrix.ui.model.getBestName +import kotlinx.collections.immutable.ImmutableList +import io.element.android.libraries.ui.strings.R as StringR + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun RoomInviteMembersView( + state: RoomInviteMembersState, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, + onSendPressed: (List) -> Unit = {}, +) { + Scaffold( + topBar = { + RoomInviteMembersTopBar( + onBackPressed = { + if (state.isSearchActive) { + state.eventSink(RoomInviteMembersEvents.OnSearchActiveChanged(false)) + } else { + onBackPressed() + } + }, + onSendPressed = { onSendPressed(state.selectedUsers) }, + canSend = state.canInvite, + ) + } + ) { padding -> + Column( + modifier = modifier + .fillMaxWidth() + .padding(padding) + .consumeWindowInsets(padding), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + RoomInviteMembersSearchBar( + modifier = Modifier.fillMaxWidth(), + query = state.searchQuery, + selectedUsers = state.selectedUsers, + state = state.searchResults, + active = state.isSearchActive, + onActiveChanged = { state.eventSink(RoomInviteMembersEvents.OnSearchActiveChanged(it)) }, + onTextChanged = { state.eventSink(RoomInviteMembersEvents.UpdateSearchQuery(it)) }, + onUserToggled = { state.eventSink(RoomInviteMembersEvents.ToggleUser(it)) }, + ) + + if (!state.isSearchActive) { + SelectedUsersList( + modifier = Modifier.fillMaxWidth(), + selectedUsers = state.selectedUsers, + autoScroll = true, + onUserRemoved = { state.eventSink(RoomInviteMembersEvents.ToggleUser(it)) }, + contentPadding = PaddingValues(16.dp), + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomInviteMembersTopBar( + canSend: Boolean, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, + onSendPressed: () -> Unit = {}, +) { + CenterAlignedTopAppBar( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.screen_room_details_invite_people_title), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ) + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + actions = { + TextButton( + onClick = onSendPressed, + content = { + Text(stringResource(StringR.string.action_send)) + }, + enabled = canSend, + ) + } + ) +} + +@Composable +private fun RoomInviteMembersSearchBar( + query: String, + state: SearchBarResultState>, + selectedUsers: ImmutableList, + active: Boolean, + modifier: Modifier = Modifier, + placeHolderTitle: String = stringResource(io.element.android.libraries.ui.strings.R.string.common_search_for_someone), + onActiveChanged: (Boolean) -> Unit = {}, + onTextChanged: (String) -> Unit = {}, + onUserToggled: (MatrixUser) -> Unit = {}, +) { + SearchBar( + query = query, + onQueryChange = onTextChanged, + active = active, + onActiveChange = onActiveChanged, + modifier = modifier, + placeHolderTitle = placeHolderTitle, + contentPrefix = { + if (selectedUsers.isNotEmpty()) { + SelectedUsersList( + modifier = Modifier.fillMaxWidth(), + selectedUsers = selectedUsers, + autoScroll = true, + onUserRemoved = onUserToggled, + contentPadding = PaddingValues(16.dp), + ) + } + }, + showBackButton = false, + resultState = state, + resultHandler = { results -> + Text( + text = "Search results", + fontWeight = FontWeight.Medium, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 8.dp) + ) + + LazyColumn { + items(results) { invitableUser -> + CheckableUserRow( + checked = invitableUser.isSelected, + enabled = !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined, + avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.MEDIUM), + name = invitableUser.matrixUser.getBestName(), + subtext = when { + // If they're already invited or joined we show that information + invitableUser.isAlreadyJoined -> stringResource(R.string.screen_room_details_already_a_member) + invitableUser.isAlreadyInvited -> stringResource(R.string.screen_room_details_already_invited) + // Otherwise show the ID, unless that's already used for their name + invitableUser.matrixUser.displayName.isNullOrEmpty().not() -> invitableUser.matrixUser.userId.value + else -> null + }, + onCheckedChange = { onUserToggled(invitableUser.matrixUser) }, + modifier = Modifier.fillMaxWidth() + ) + } + } + }, + ) +} + +@Preview +@Composable +fun RoomInviteMembersLightPreview(@PreviewParameter(RoomInviteMembersStateProvider::class) state: RoomInviteMembersState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun RoomInviteMembersDarkPreview(@PreviewParameter(RoomInviteMembersStateProvider::class) state: RoomInviteMembersState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: RoomInviteMembersState) { + RoomInviteMembersView(state) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index 5b1e7a72e0..f37ea1229b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -27,7 +27,6 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.RoomMember @ContributesNode(RoomScope::class) class RoomMemberListNode @AssistedInject constructor( @@ -38,6 +37,7 @@ class RoomMemberListNode @AssistedInject constructor( interface Callback : Plugin { fun openRoomMemberDetails(roomMemberId: UserId) + fun openInviteMembers() } private val callbacks = plugins() @@ -48,6 +48,12 @@ class RoomMemberListNode @AssistedInject constructor( } } + private fun openInviteMembers() { + callbacks.forEach { + it.openInviteMembers() + } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -56,6 +62,7 @@ class RoomMemberListNode @AssistedInject constructor( modifier = modifier, onBackPressed = { navigateUp() }, onMemberSelected = this::openRoomMemberDetails, + onInvitePressed = this::openInviteMembers, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index ddf7864b96..36128bd638 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -18,6 +18,8 @@ package io.element.android.features.roomdetails.impl.members import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -27,12 +29,15 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.withContext import javax.inject.Inject class RoomMemberListPresenter @Inject constructor( + private val room: MatrixRoom, private val roomMemberListDataSource: RoomMemberListDataSource, private val coroutineDispatchers: CoroutineDispatchers, ) : Presenter { @@ -46,6 +51,9 @@ class RoomMemberListPresenter @Inject constructor( } var isSearchActive by rememberSaveable { mutableStateOf(false) } + val membersState by room.membersStateFlow.collectAsState() + val canInvite by getCanInvite(membersState = membersState) + LaunchedEffect(Unit) { withContext(coroutineDispatchers.io) { val members = roomMemberListDataSource.search("").groupBy { it.membership } @@ -80,6 +88,7 @@ class RoomMemberListPresenter @Inject constructor( searchQuery = searchQuery, searchResults = searchResults, isSearchActive = isSearchActive, + canInvite = canInvite, eventSink = { event -> when (event) { is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active @@ -88,5 +97,14 @@ class RoomMemberListPresenter @Inject constructor( }, ) } + + @Composable + private fun getCanInvite(membersState: MatrixRoomMembersState): State { + val canInvite = remember(membersState) { mutableStateOf(false) } + LaunchedEffect(membersState) { + canInvite.value = room.canInvite().getOrElse { false } + } + return canInvite + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt index 93e2142a19..f718db703b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -26,6 +26,7 @@ data class RoomMemberListState( val searchQuery: String, val searchResults: SearchBarResultState, val isSearchActive: Boolean, + val canInvite: Boolean, val eventSink: (RoomMemberListEvents) -> Unit, ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index 9bc8c00a1e..f1e6447a10 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -36,6 +36,7 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider Unit, + onInvitePressed: () -> Unit, onMemberSelected: (UserId) -> Unit, modifier: Modifier = Modifier, ) { @@ -79,7 +81,11 @@ fun RoomMemberListView( Scaffold( topBar = { if (!state.isSearchActive) { - RoomMemberListTopBar(onBackPressed = onBackPressed) + RoomMemberListTopBar( + canInvite = state.canInvite, + onBackPressed = onBackPressed, + onInvitePressed = onInvitePressed, + ) } } ) { padding -> @@ -192,8 +198,10 @@ private fun RoomMemberListItem( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun RoomMemberListTopBar( + canInvite: Boolean, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, + onInvitePressed: () -> Unit = {}, ) { CenterAlignedTopAppBar( modifier = modifier, @@ -205,6 +213,19 @@ private fun RoomMemberListTopBar( ) }, navigationIcon = { BackButton(onClick = onBackPressed) }, + actions = { + if (canInvite) { + TextButton( + modifier = Modifier.padding(horizontal = 8.dp), + onClick = onInvitePressed, + ) { + Text( + text = stringResource(StringR.string.action_invite), + fontSize = 16.sp, + ) + } + } + } ) } @@ -252,6 +273,7 @@ private fun ContentToPreview(state: RoomMemberListState) { RoomMemberListView( state = state, onBackPressed = {}, - onMemberSelected = {} + onMemberSelected = {}, + onInvitePressed = {}, ) } diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index 156261f66a..393052591f 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -6,9 +6,11 @@ "Already a member" "Already invited" + "An error occurred when updating the room details" "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them." "Message encryption enabled" "Share room" + "Updating room…" "Pending" "Room members" "Block" diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index b3717f4244..8e97c42936 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -25,7 +25,6 @@ import io.element.android.features.roomdetails.impl.RoomDetailsEvent import io.element.android.features.roomdetails.impl.RoomDetailsPresenter import io.element.android.features.roomdetails.impl.RoomDetailsType import io.element.android.features.roomdetails.impl.members.aRoomMember -import io.element.android.features.roomdetails.impl.members.aRoomMemberList import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId @@ -33,7 +32,6 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState -import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange @@ -101,18 +99,19 @@ class RoomDetailsPresenterTests { room.givenRoomMembersState(MatrixRoomMembersState.Unknown) val initialState = awaitItem() Truth.assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized) + skipItems(1) room.givenRoomMembersState(MatrixRoomMembersState.Pending(null)) val loadingState = awaitItem() Truth.assertThat(loadingState.memberCount).isEqualTo(Async.Loading(null)) room.givenRoomMembersState(MatrixRoomMembersState.Error(error)) - //skipItems(1) + skipItems(1) val failureState = awaitItem() Truth.assertThat(failureState.memberCount).isEqualTo(Async.Failure(error, null)) room.givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) - //skipItems(1) + skipItems(1) val successState = awaitItem() Truth.assertThat(successState.memberCount).isEqualTo(Async.Success(1)) @@ -166,6 +165,8 @@ class RoomDetailsPresenterTests { presenter.present() }.test { val initialState = awaitItem() + skipItems(1) + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) val confirmationState = awaitItem() Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.PrivateRoom) @@ -182,6 +183,8 @@ class RoomDetailsPresenterTests { presenter.present() }.test { val initialState = awaitItem() + skipItems(1) + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) val confirmationState = awaitItem() Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.LastUserInRoom) @@ -198,6 +201,8 @@ class RoomDetailsPresenterTests { presenter.present() }.test { val initialState = awaitItem() + skipItems(1) + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) val confirmationState = awaitItem() Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.Generic) @@ -214,6 +219,8 @@ class RoomDetailsPresenterTests { presenter.present() }.test { val initialState = awaitItem() + skipItems(1) + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) cancelAndIgnoreRemainingEvents() @@ -235,6 +242,8 @@ class RoomDetailsPresenterTests { presenter.present() }.test { val initialState = awaitItem() + skipItems(1) + initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) val errorState = awaitItem() Truth.assertThat(errorState.error).isNotNull() @@ -242,6 +251,50 @@ class RoomDetailsPresenterTests { Truth.assertThat(awaitItem().error).isNull() } } + + @Test + fun `present - initial state when user can invite others to room`() = runTest { + val room = aMatrixRoom().apply { + givenCanInviteResult(Result.success(true)) + } + val presenter = aRoomDetailsPresenter(room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + // Initially false + Truth.assertThat(awaitItem().canInvite).isFalse() + // Then the asynchronous check completes and it becomes true + Truth.assertThat(awaitItem().canInvite).isTrue() + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - initial state when user can not invite others to room`() = runTest { + val room = aMatrixRoom().apply { + givenCanInviteResult(Result.success(false)) + } + val presenter = aRoomDetailsPresenter(room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + Truth.assertThat(awaitItem().canInvite).isFalse() + } + } + + @Test + fun `present - initial state when canInvite errors`() = runTest { + val room = aMatrixRoom().apply { + givenCanInviteResult(Result.failure(Throwable("Whoops"))) + } + val presenter = aRoomDetailsPresenter(room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + Truth.assertThat(awaitItem().canInvite).isFalse() + } + } } fun aMatrixClient( diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt new file mode 100644 index 0000000000..49ecff7d3b --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.invite + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomdetails.aMatrixRoom +import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource +import io.element.android.features.roomdetails.impl.members.aRoomMember +import io.element.android.features.roomdetails.impl.members.aRoomMemberList +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import io.element.android.libraries.usersearch.test.FakeUserRepository +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class RoomInviteMembersPresenterTest { + + @Test + fun `present - initial state has no results and no search`() = runTest { + val presenter = RoomInviteMembersPresenter( + userRepository = FakeUserRepository(), + roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + + assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java) + assertThat(initialState.isSearchActive).isFalse() + assertThat(initialState.canInvite).isFalse() + assertThat(initialState.searchQuery).isEmpty() + + skipItems(1) + } + } + + @Test + fun `present - updates search active state`() = runTest { + val presenter = RoomInviteMembersPresenter( + userRepository = FakeUserRepository(), + roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + initialState.eventSink(RoomInviteMembersEvents.OnSearchActiveChanged(true)) + + val resultState = awaitItem() + assertThat(resultState.isSearchActive).isTrue() + } + } + + @Test + fun `present - performs search and handles no results`() = runTest { + val repository = FakeUserRepository() + val presenter = RoomInviteMembersPresenter( + userRepository = repository, + roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query")) + skipItems(1) + + assertThat(repository.providedQuery).isEqualTo("some query") + repository.emitResult(emptyList()) + skipItems(1) + + val resultState = awaitItem() + assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.NoResults::class.java) + } + } + + @Test + fun `present - performs search and handles user results`() = runTest { + val repository = FakeUserRepository() + val presenter = RoomInviteMembersPresenter( + userRepository = repository, + roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query")) + skipItems(1) + + assertThat(repository.providedQuery).isEqualTo("some query") + repository.emitResult(aMatrixUserList()) + skipItems(1) + + val resultState = awaitItem() + assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + + val expectedUsers = aMatrixUserList() + val users = resultState.searchResults.users() + expectedUsers.forEachIndexed { index, matrixUser -> + assertThat(users[index].matrixUser).isEqualTo(matrixUser) + assertThat(users[index].isAlreadyInvited).isFalse() + assertThat(users[index].isAlreadyJoined).isFalse() + assertThat(users[index].isSelected).isFalse() + } + } + } + + @Test + fun `present - performs search and handles membership state of existing users`() = runTest { + val userList = aMatrixUserList() + val joinedUser = userList[0] + val invitedUser = userList[1] + + val repository = FakeUserRepository() + val presenter = RoomInviteMembersPresenter( + userRepository = repository, + roomMemberListDataSource = createDataSource(FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(listOf( + aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN), + aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE), + ))) + }), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query")) + skipItems(1) + + assertThat(repository.providedQuery).isEqualTo("some query") + repository.emitResult(aMatrixUserList()) + skipItems(1) + + val resultState = awaitItem() + assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + + val users = resultState.searchResults.users() + + // The result that matches a user with JOINED membership is marked as such + val userWhoShouldBeJoined = users.find { it.matrixUser == joinedUser } + assertThat(userWhoShouldBeJoined).isNotNull() + assertThat(userWhoShouldBeJoined?.isAlreadyJoined).isTrue() + assertThat(userWhoShouldBeJoined?.isAlreadyInvited).isFalse() + + // The result that matches a user with INVITED membership is marked as such + val userWhoShouldBeInvited = users.find { it.matrixUser == invitedUser } + assertThat(userWhoShouldBeInvited).isNotNull() + assertThat(userWhoShouldBeInvited?.isAlreadyJoined).isFalse() + assertThat(userWhoShouldBeInvited?.isAlreadyInvited).isTrue() + + // All other users are neither joined nor invited + val otherUsers = users.minus(userWhoShouldBeInvited!!).minus(userWhoShouldBeJoined!!) + assertThat(otherUsers.none { it.isAlreadyInvited }).isTrue() + assertThat(otherUsers.none { it.isAlreadyJoined }).isTrue() + } + } + + @Test + fun `present - toggle users updates selected user state`() = runTest { + val repository = FakeUserRepository() + val presenter = RoomInviteMembersPresenter( + userRepository = repository, + roomMemberListDataSource = createDataSource(), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + // When we toggle a user not in the list, they are added + initialState.eventSink(RoomInviteMembersEvents.ToggleUser(aMatrixUser())) + assertThat(awaitItem().selectedUsers).containsExactly(aMatrixUser()) + + // Toggling a different user also adds them + initialState.eventSink(RoomInviteMembersEvents.ToggleUser(aMatrixUser(id = A_USER_ID_2.value))) + assertThat(awaitItem().selectedUsers).containsExactly(aMatrixUser(), aMatrixUser(id = A_USER_ID_2.value)) + + // Toggling the first user removes them + initialState.eventSink(RoomInviteMembersEvents.ToggleUser(aMatrixUser())) + assertThat(awaitItem().selectedUsers).containsExactly(aMatrixUser(id = A_USER_ID_2.value)) + } + } + + @Test + fun `present - selected users appear as such in search results`() = runTest { + val repository = FakeUserRepository() + val presenter = RoomInviteMembersPresenter( + userRepository = repository, + roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + val selectedUser = aMatrixUser() + + initialState.eventSink(RoomInviteMembersEvents.ToggleUser(selectedUser)) + + initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query")) + skipItems(1) + + assertThat(repository.providedQuery).isEqualTo("some query") + repository.emitResult(aMatrixUserList() + selectedUser) + skipItems(2) + + val resultState = awaitItem() + assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + + val users = resultState.searchResults.users() + + // The one user we have previously toggled is marked as selected + val shouldBeSelectedUser = users.find { it.matrixUser == selectedUser } + assertThat(shouldBeSelectedUser).isNotNull() + assertThat(shouldBeSelectedUser?.isSelected).isTrue() + + // And no others are + val allOtherUsers = users.minus(shouldBeSelectedUser!!) + assertThat(allOtherUsers.none { it.isSelected }).isTrue() + } + } + + + @Test + fun `present - toggling a user updates existing search results`() = runTest { + val repository = FakeUserRepository() + val presenter = RoomInviteMembersPresenter( + userRepository = repository, + roomMemberListDataSource = createDataSource(FakeMatrixRoom()), + coroutineDispatchers = testCoroutineDispatchers() + ) + + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + + val selectedUser = aMatrixUser() + + // Given a query is made + initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query")) + skipItems(1) + + assertThat(repository.providedQuery).isEqualTo("some query") + repository.emitResult(aMatrixUserList() + selectedUser) + skipItems(2) + + // And then a user is toggled + initialState.eventSink(RoomInviteMembersEvents.ToggleUser(selectedUser)) + skipItems(1) + val resultState = awaitItem() + + // The results are updated... + assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + val users = resultState.searchResults.users() + + // The one user we have now toggled is marked as selected + val shouldBeSelectedUser = users.find { it.matrixUser == selectedUser } + assertThat(shouldBeSelectedUser).isNotNull() + assertThat(shouldBeSelectedUser?.isSelected).isTrue() + + // And no others are + val allOtherUsers = users.minus(shouldBeSelectedUser!!) + assertThat(allOtherUsers.none { it.isSelected }).isTrue() + } + } + + private fun createDataSource( + matrixRoom: MatrixRoom = aMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + }, + coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers() + ) = RoomMemberListDataSource(matrixRoom, coroutineDispatchers) + + private fun SearchBarResultState>.users() = + (this as? SearchBarResultState.Results>)?.results.orEmpty() +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 73c873701e..9625a167f7 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -32,6 +32,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -113,6 +114,54 @@ class RoomMemberListPresenterTests { } } + + @Test + fun `present - asynchronously sets canInvite when user has correct power level`() = runTest { + val presenter = createPresenter( + matrixRoom = FakeMatrixRoom().apply { + givenCanInviteResult(Result.success(true)) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val loadedState = awaitItem() + Truth.assertThat(loadedState.canInvite).isTrue() + } + } + + @Test + fun `present - asynchronously sets canInvite when user does not have correct power level`() = runTest { + val presenter = createPresenter( + matrixRoom = FakeMatrixRoom().apply { + givenCanInviteResult(Result.success(false)) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val loadedState = awaitItem() + Truth.assertThat(loadedState.canInvite).isFalse() + } + } + + @Test + fun `present - asynchronously sets canInvite when power level check fails`() = runTest { + val presenter = createPresenter( + matrixRoom = FakeMatrixRoom().apply { + givenCanInviteResult(Result.failure(Throwable("Eek"))) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val loadedState = awaitItem() + Truth.assertThat(loadedState.canInvite).isFalse() + } + } } @ExperimentalCoroutinesApi @@ -125,6 +174,7 @@ private fun createDataSource( @ExperimentalCoroutinesApi private fun createPresenter( + matrixRoom: MatrixRoom = FakeMatrixRoom(), roomMemberListDataSource: RoomMemberListDataSource = createDataSource(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers() -) = RoomMemberListPresenter(roomMemberListDataSource, coroutineDispatchers) +) = RoomMemberListPresenter(matrixRoom, roomMemberListDataSource, coroutineDispatchers) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a50f2a631..96a384d397 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -118,7 +118,7 @@ test_hamcrest = "org.hamcrest:hamcrest:2.2" test_orchestrator = "androidx.test:orchestrator:1.4.2" test_turbine = "app.cash.turbine:turbine:0.12.3" test_truth = "com.google.truth:truth:1.1.3" -test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.11" +test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.12" test_robolectric = "org.robolectric:robolectric:4.10.3" test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } @@ -133,7 +133,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.14" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.15" sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt index 8d91404d29..3338a218cb 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt @@ -57,6 +57,7 @@ fun SearchBar( placeHolderTitle: String, modifier: Modifier = Modifier, enabled: Boolean = true, + showBackButton: Boolean = true, resultState: SearchBarResultState = SearchBarResultState.NotSearching(), shape: Shape = SearchBarDefaults.inputFieldShape, tonalElevation: Dp = SearchBarDefaults.Elevation, @@ -87,7 +88,7 @@ fun SearchBar( modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine) ) }, - leadingIcon = if (active) { + leadingIcon = if (showBackButton && active) { { BackButton(onClick = { onActiveChange(false) }) } } else { null @@ -179,6 +180,16 @@ internal fun SearchBarPreviewActiveWithQuery() = ElementThemedPreview { ) } +@Preview(group = PreviewGroup.Search) +@Composable +internal fun SearchBarPreviewActiveWithQueryNoBackButton() = ElementThemedPreview { + ContentToPreview( + query = "search term", + active = true, + showBackButton = false, + ) +} + @Preview(group = PreviewGroup.Search) @Composable internal fun SearchBarPreviewActiveWithNoResults() = ElementThemedPreview { @@ -212,6 +223,7 @@ internal fun SearchBarPreviewActiveWithContent() = ElementThemedPreview { private fun ContentToPreview( query: String = "", active: Boolean = false, + showBackButton: Boolean = true, resultState: SearchBarResultState = SearchBarResultState.NotSearching(), contentPrefix: @Composable ColumnScope.() -> Unit = {}, contentSuffix: @Composable ColumnScope.() -> Unit = {}, @@ -221,6 +233,7 @@ private fun ContentToPreview( query = query, active = active, resultState = resultState, + showBackButton = showBackButton, onQueryChange = {}, onActiveChange = {}, placeHolderTitle = "Search for things", diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 90b55a3a42..5cf9375e0a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -85,4 +85,8 @@ interface MatrixRoom : Closeable { suspend fun acceptInvitation(): Result suspend fun rejectInvitation(): Result + + suspend fun inviteUserById(id: UserId): Result + + suspend fun canInvite(): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventSendState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventSendState.kt index 8a3af60903..d41bc2edc0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventSendState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventSendState.kt @@ -19,7 +19,7 @@ package io.element.android.libraries.matrix.api.timeline.item.event import io.element.android.libraries.matrix.api.core.EventId sealed interface EventSendState { - object NotSendYet : EventSendState + object NotSentYet : EventSendState data class SendingFailed( val error: String diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index b3dc10ab9f..181c176802 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -58,7 +58,6 @@ import org.matrix.rustcomponents.sdk.RequiredState import org.matrix.rustcomponents.sdk.SlidingSyncList import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder import org.matrix.rustcomponents.sdk.SlidingSyncListOnceBuilt -import org.matrix.rustcomponents.sdk.SlidingSyncMode import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.use @@ -118,7 +117,7 @@ class RustMatrixClient constructor( ) ) .filters(visibleRoomsSlidingSyncFilters) - .syncMode(mode = SlidingSyncMode.SELECTIVE) + .syncModeSelective() .addRange(0u, 20u) .onceBuilt(object : SlidingSyncListOnceBuilt { override fun updateList(list: SlidingSyncList): SlidingSyncList { @@ -140,7 +139,7 @@ class RustMatrixClient constructor( ) ) .filters(invitesSlidingSyncFilters) - .syncMode(mode = SlidingSyncMode.SELECTIVE) + .syncModeSelective() .addRange(0u, 20u) .onceBuilt(object : SlidingSyncListOnceBuilt { override fun updateList(list: SlidingSyncList): SlidingSyncList { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 8bd6080ab3..55576c7b96 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.SlidingSyncRoom import org.matrix.rustcomponents.sdk.UpdateSummary import org.matrix.rustcomponents.sdk.genTransactionId @@ -209,6 +210,18 @@ class RustMatrixRoom( } } + override suspend fun inviteUserById(id: UserId): Result = withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.inviteUserById(id.value) + } + } + + override suspend fun canInvite(): Result = withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.member(sessionId.value).use(RoomMember::canInvite) + } + } + override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result = withContext(coroutineDispatchers.io) { runCatching { innerRoom.sendImage(file.path, thumbnailFile.path, imageInfo.map()) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 6f64db76ef..4884dc2d84 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -152,10 +152,12 @@ class RustMatrixTimeline( RequiredState(key = "m.room.canonical_alias", value = ""), RequiredState(key = "m.room.topic", value = ""), RequiredState(key = "m.room.join_rules", value = ""), + RequiredState(key = "m.room.power_levels", value = ""), ), timelineLimit = null ) - val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings) + listenerTokens += slidingSyncRoom.subscribeToRoom(settings) + val result = slidingSyncRoom.addTimelineListener(timelineListener) launch { fetchMembers() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index 726b448099..0f6a818cca 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -63,7 +63,7 @@ fun RustProfileDetails.map(): ProfileTimelineDetails { fun RustEventSendState?.map(): EventSendState? { return when (this) { null -> null - RustEventSendState.NotSendYet -> EventSendState.NotSendYet + RustEventSendState.NotSentYet -> EventSendState.NotSentYet is RustEventSendState.SendingFailed -> EventSendState.SendingFailed(error) is RustEventSendState.Sent -> EventSendState.Sent(EventId(eventId)) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 88d23d5b91..ff91db14b3 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -60,6 +60,8 @@ class FakeMatrixRoom( private var updateMembersResult: Result = Result.success(Unit) private var acceptInviteResult = Result.success(Unit) private var rejectInviteResult = Result.success(Unit) + private var inviteUserResult = Result.success(Unit) + private var canInviteResult = Result.success(true) private var sendMediaResult = Result.success(Unit) var sendMediaCount = 0 private set @@ -70,6 +72,9 @@ class FakeMatrixRoom( var isInviteRejected: Boolean = false private set + var invitedUserId: UserId? = null + private set + private var leaveRoomError: Throwable? = null override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) @@ -137,6 +142,15 @@ class FakeMatrixRoom( return rejectInviteResult } + override suspend fun inviteUserById(id: UserId): Result { + invitedUserId = id + return inviteUserResult + } + + override suspend fun canInvite(): Result { + return canInviteResult + } + override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result = fakeSendMedia() override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result = fakeSendMedia() @@ -182,6 +196,14 @@ class FakeMatrixRoom( rejectInviteResult = result } + fun givenInviteUserResult(result: Result) { + inviteUserResult = result + } + + fun givenCanInviteResult(result: Result) { + canInviteResult = result + } + fun givenIgnoreResult(result: Result) { ignoreResult = result } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 40468f8a05..ca2bcd1d28 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -23,6 +23,7 @@ "Edit" "Enable" "Invite" + "Invite friends" "Invite friends to %1$s" "Invites" "Learn more" @@ -69,6 +70,7 @@ "File" "GIF" "Image" + "Leaving room" "Link copied to clipboard" "Loading…" "Message" @@ -98,6 +100,8 @@ "Suggestions" "Topic" "Unable to decrypt" + "We were unable to successfully send invites to one or more users." + "Unable to send invite(s)" "Unsupported event" "Username" "Verification cancelled" @@ -118,6 +122,7 @@ "Failed loading messages" "Some messages have not been sent" "Sorry, an error occurred" + "πŸ”οΈ Join me on %1$s" "Hey, talk to me on %1$s: %2$s" "Are you sure that you want to leave this room? You are the only person here. If you leave, no one will be able to join in the future, including you." "Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite." @@ -162,4 +167,4 @@ "You can read all our terms %1$s." "here" "Block user" - + \ No newline at end of file diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index e7023ad88e..c433ed2e07 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -95,6 +95,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { fun DependencyHandlerScope.allServicesImpl() { implementation(project(":services:analytics:noop")) + implementation(project(":services:apperror:impl")) implementation(project(":services:appnavstate:impl")) implementation(project(":services:toolbox:impl")) } diff --git a/services/apperror/api/build.gradle.kts b/services/apperror/api/build.gradle.kts new file mode 100644 index 0000000000..94970d9774 --- /dev/null +++ b/services/apperror/api/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.services.apperror.api" +} + +dependencies { + implementation(libs.coroutines.core) +} diff --git a/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt new file mode 100644 index 0000000000..c808ebe503 --- /dev/null +++ b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.services.apperror.api + +sealed interface AppErrorState { + + object NoError : AppErrorState + + data class Error( + val title: String, + val body: String, + val dismiss: () -> Unit, + ) : AppErrorState + +} diff --git a/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateProvider.kt b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateProvider.kt new file mode 100644 index 0000000000..50d857645e --- /dev/null +++ b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateProvider.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.services.apperror.api + +fun aAppErrorState() = AppErrorState.Error( + title = "An error occurred", + body = "Something went wrong, and the details of that would go here.", + dismiss = {}, +) diff --git a/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateService.kt b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateService.kt new file mode 100644 index 0000000000..b1f9b97ac2 --- /dev/null +++ b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateService.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.services.apperror.api + +import kotlinx.coroutines.flow.StateFlow + +interface AppErrorStateService { + + val appErrorStateFlow: StateFlow + + fun showError(title: String, body: String) + +} diff --git a/services/apperror/impl/build.gradle.kts b/services/apperror/impl/build.gradle.kts new file mode 100644 index 0000000000..285577de9a --- /dev/null +++ b/services/apperror/impl/build.gradle.kts @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.ksp) + alias(libs.plugins.anvil) +} + +anvil { + generateDaggerFactories.set(true) +} + +android { + namespace = "io.element.android.services.apperror.impl" +} + +dependencies { + anvil(projects.anvilcodegen) + implementation(libs.dagger) + implementation(projects.libraries.core) + implementation(projects.libraries.di) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + implementation(projects.anvilannotations) + + implementation(libs.coroutines.core) + implementation(libs.androidx.corektx) + + api(projects.services.apperror.api) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.test.turbine) + testImplementation(libs.test.truth) + + ksp(libs.showkase.processor) +} diff --git a/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt b/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt new file mode 100644 index 0000000000..ac5d4ee75d --- /dev/null +++ b/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.services.apperror.impl + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.services.apperror.api.AppErrorState +import io.element.android.services.apperror.api.aAppErrorState + +@Composable +fun AppErrorView( + state: AppErrorState, +) { + if (state is AppErrorState.Error) { + AppErrorViewContent( + title = state.title, + body = state.body, + onDismiss = state.dismiss, + ) + } +} + +@Composable +fun AppErrorViewContent( + title: String, + body: String, + onDismiss: () -> Unit = { }, +) { + ErrorDialog( + title = title, + content = body, + onDismiss = onDismiss, + ) +} + +@Preview +@Composable +internal fun AppErrorViewLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun AppErrorViewDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + AppErrorView( + state = aAppErrorState() + ) +} diff --git a/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateService.kt b/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateService.kt new file mode 100644 index 0000000000..813c00cd65 --- /dev/null +++ b/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateService.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.services.apperror.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import io.element.android.services.apperror.api.AppErrorState +import io.element.android.services.apperror.api.AppErrorStateService +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) +class DefaultAppErrorStateService @Inject constructor() : AppErrorStateService { + + private val currentAppErrorState = MutableStateFlow(AppErrorState.NoError) + override val appErrorStateFlow: StateFlow = currentAppErrorState + + override fun showError(title: String, body: String) { + currentAppErrorState.value = AppErrorState.Error( + title = title, + body = body, + dismiss = { + currentAppErrorState.value = AppErrorState.NoError + }, + ) + } +} diff --git a/services/apperror/impl/src/test/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateServiceTest.kt b/services/apperror/impl/src/test/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateServiceTest.kt new file mode 100644 index 0000000000..71c20aa8a2 --- /dev/null +++ b/services/apperror/impl/src/test/kotlin/io/element/android/services/apperror/impl/DefaultAppErrorStateServiceTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.services.apperror.impl + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.services.apperror.api.AppErrorState +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class DefaultAppErrorStateServiceTest { + + @Test + fun `initial value is no error`() = runTest { + val service = DefaultAppErrorStateService() + + service.appErrorStateFlow.test { + val state = awaitItem() + assertThat(state).isInstanceOf(AppErrorState.NoError::class.java) + } + } + + @Test + fun `showError - emits value`() = runTest { + val service = DefaultAppErrorStateService() + + service.appErrorStateFlow.test { + skipItems(1) + + service.showError("Title", "Body") + val state = awaitItem() + assertThat(state).isInstanceOf(AppErrorState.Error::class.java) + + val errorState = state as AppErrorState.Error + assertThat(errorState.title).isEqualTo("Title") + assertThat(errorState.body).isEqualTo("Body") + } + } + + @Test + fun `dismiss - clears value`() = runTest { + val service = DefaultAppErrorStateService() + + service.appErrorStateFlow.test { + skipItems(1) + + service.showError("Title", "Body") + val state = awaitItem() + assertThat(state).isInstanceOf(AppErrorState.Error::class.java) + + val errorState = state as AppErrorState.Error + errorState.dismiss() + + assertThat(awaitItem()).isInstanceOf(AppErrorState.NoError::class.java) + } + } + +} diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 8b87a5a644..ebc59dec6a 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56148beb26b35c8309190271b43fc225e11b90b8b849a8e60abf98b6ab663c1b -size 101010 +oid sha256:439a2d9e9497486e86b63fe42402f1c2ee33b15695e47312e18867530d041dad +size 96539 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index ba7682c859..dd593bdc3e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fa60afaf7ab23f66622d7d77e168db8b9d8bc15d178e8adb63f944de3cc29df -size 96242 +oid sha256:255b99dea5c1da9d466533d510d7c0abe39a79cdc42b579cfa186176e9c6a49d +size 91991 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ee9f0fb113 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:516e972069292f31625847ca39de06ea402ce1606c97d84f930c18eaa2448cfa +size 13759 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a1094a6fa6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b387f10903926ba9ed30b7d213314388eebf23743cbbdbcf4b613cf3136f64aa +size 41524 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..79b48dd535 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0af8c7149fd4266c68c0e4b8b9a699bc0b3fc0b05fea3f2e2544abc884f70c5b +size 11988 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ff95c9ed19 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8eb2fcdb57b7f1ba28990f9576fad6dce156fe0c5c9ae491797040fc22ae4f7b +size 39516 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f7f16c2b8a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e4d3bc53e6a86957218247b614be6bbc78cfb35ce49565f2243f50d6ef94fe3 +size 14216 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0cd4f9c3e0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:213777e4ace6f9c33209db37bffff2ca6dba13e540e9d07be82239d261ee8e3f +size 63951 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..57c7799447 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ddec11fd636d345c49cf981ce7498ac2d94027d4b223b257b64288bb0254766 +size 13328 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1db9b42586 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da5ba4637d999c8654e38a86ad0b26c75a6fe10a6988eb821b24cf91da2274e5 +size 39070 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6648389b0e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c57c20fe79d0dbb0ca25571fa2ab3ba6e06f3a9c6634e96fac3ed0155c2ea106 +size 11160 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5b6a14ea66 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e43279132a7536516cf267599f877f7c729f76be51d6319944f74c57af0e282 +size 36493 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fd948f750d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:933c5ac4478699e9e713af559025ab675a8bdc93b15c42ff27fe5525fa3b668a +size 13166 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a35148eff1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a66a39bf18ba35abfc87dea22ca019eea98727bf246142cb034649c5b1772bd +size 61041 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_2,NEXUS_5,1.0,en].png index d03094bab0..28c2f7631d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49e7533e6afba903219942193e0cc5cfbe2f67ba3b57516f77c259e7c42e8e3f -size 11813 +oid sha256:bbe60787ea821d25168d4a6d75000629c9c0d505a2c8eec9ba3af57790bf4b0a +size 12863 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_3,NEXUS_5,1.0,en].png index f13a65f8e2..d03094bab0 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89ea65099fb4981bbeb24e49afb7400b0e8da79e3b783e198519ba1e970404a8 -size 8317 +oid sha256:49e7533e6afba903219942193e0cc5cfbe2f67ba3b57516f77c259e7c42e8e3f +size 11813 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_4,NEXUS_5,1.0,en].png index 74a7f599cd..f13a65f8e2 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a8ba207cf61c56b64c6855d04c8a30def0b3daf325adf57edd45b40981ed745 -size 7733 +oid sha256:89ea65099fb4981bbeb24e49afb7400b0e8da79e3b783e198519ba1e970404a8 +size 8317 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_5,NEXUS_5,1.0,en].png index 22fe768986..74a7f599cd 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:070c93168058fded5a76e49050f6c4554c7fe483a19aae13c1af4426a6b575f1 -size 30582 +oid sha256:9a8ba207cf61c56b64c6855d04c8a30def0b3daf325adf57edd45b40981ed745 +size 7733 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_6,NEXUS_5,1.0,en].png index 0f9a5a79cc..22fe768986 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5b879b74654fdad0638f432a71adfc9186fbe00dafd5d963a3affb33ec8c5c8 -size 12841 +oid sha256:070c93168058fded5a76e49050f6c4554c7fe483a19aae13c1af4426a6b575f1 +size 30582 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0f9a5a79cc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5b879b74654fdad0638f432a71adfc9186fbe00dafd5d963a3affb33ec8c5c8 +size 12841 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_2,NEXUS_5,1.0,en].png index 3d261cb40b..fbe3e96f54 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c3abebbe9e55706af5f4d1089e4308b0e975258a9d6a0e1793a4208b36c9c93 -size 11754 +oid sha256:925e60112749ce49cf1c2d855406860375357e4717cb7487d3b8bb9a94d7e816 +size 12882 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_3,NEXUS_5,1.0,en].png index eb23dcec96..3d261cb40b 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8de2f28cf9918a7eddcc1311c34ba0376242a4708724b57689df000b7480524 -size 8197 +oid sha256:9c3abebbe9e55706af5f4d1089e4308b0e975258a9d6a0e1793a4208b36c9c93 +size 11754 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_4,NEXUS_5,1.0,en].png index e2e4a33e67..eb23dcec96 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:687133e4729ea42382ac0b76af6ceaf8b20cbc65923412fb97b077d2475c70f7 -size 7514 +oid sha256:b8de2f28cf9918a7eddcc1311c34ba0376242a4708724b57689df000b7480524 +size 8197 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_5,NEXUS_5,1.0,en].png index 1b35815820..e2e4a33e67 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9dd67135f57c6e8a336e238384b02cd6068770b84f429f357d03d5e1e4c22ab6 -size 29286 +oid sha256:687133e4729ea42382ac0b76af6ceaf8b20cbc65923412fb97b077d2475c70f7 +size 7514 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_6,NEXUS_5,1.0,en].png index 787c71d87e..1b35815820 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:534cba0c13aa52b3557ab8df854c521dd13b82e5a1550d73abcef12925e389da -size 11878 +oid sha256:9dd67135f57c6e8a336e238384b02cd6068770b84f429f357d03d5e1e4c22ab6 +size 29286 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..787c71d87e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:534cba0c13aa52b3557ab8df854c521dd13b82e5a1550d73abcef12925e389da +size 11878 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png index bb0a5b2211..3cd7e20f05 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a755d1793d5a77244abe66b8a72521d64c46c102e9e9c60d1839d793301526d -size 39373 +oid sha256:72f159556f6ccee83c65589b882b3ac9ca9d4f4de8e611f31e19a524e22eab8e +size 37569 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png index ded656a069..090890f1ad 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04ffb414eeb75e4fb4187777be04c3fc3a9a0967c81a2150ea2d4f58ef4212bc -size 30599 +oid sha256:6b1ac0c5e9fc3ecb9870ed3322418c26e26ec0873806d3eb7a89b2a51b39c1f1 +size 28563 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png index 1a92b42c6b..48ead02bc6 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5ccb63879a3b9a4d37a38903a5b81cf1a185afc638f2d0260b6a6fe68aa2419 -size 28779 +oid sha256:aa8e818a0c6175cc40b888f8c5a093655b8a352cc2b90e7159d9317de8f68e8f +size 26778 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png index 6a8e4b6ac0..29b6bcc2c0 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6b5488817b1262bc17800567b3634861230f6db3b81fbe1e91f26d6795a44ae -size 36828 +oid sha256:54b897538574ecb5a6e7a13b0f3ae8d0495e8db4667f1878dfc0ca354d0faf47 +size 34903 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png index 622b3a6def..06ff0e03c3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d1d7c27b6c1b3ca495fe794ca888e55f013b8c99b716f4571f8c3c278ed85d7 -size 38995 +oid sha256:1d5ae33b866c962c0a444b6893dc602b99943a2305a3c3e9695219c1a33388bb +size 37208 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bb0a5b2211 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a755d1793d5a77244abe66b8a72521d64c46c102e9e9c60d1839d793301526d +size 39373 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png index 85b1b93a66..3e9cbb975a 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:333798d610045630944e53c4c43bc401781cc5499ad8c085037239a93de6eccc -size 36950 +oid sha256:8912f6213b14f6380359582e96901ee5a1dcdf9f939de3ac8d5bbb6a62f25eaa +size 35190 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png index c0a142af4b..4d1b84cdb3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0206a86b0db1d4c2a984435fda5b70335fa3f005cc4c49a3da7d1f3f80fb04e6 -size 29057 +oid sha256:ad68e0852e7742b58d976a1d8ca2e2da66bf5477c0876ecc9e2c1007cf4b843f +size 27197 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png index 8c2ed2d853..a4c174b08f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d55124590cfc79d4c85a35de16223ff7b7821c32742670ea12af0bea05a6ef87 -size 27081 +oid sha256:048309d5c82ff3ea6dcdd1e5bc81103b5d9147083089f7a006fce840b1d23867 +size 25445 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png index 7fe6e59dd6..4575ec4932 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fadd36fcc359c5c20d21f1525807d07487409f736e9b82d79d261c1e1a48dd4e -size 34406 +oid sha256:ff28aa87583eb9ec9966f761deec56757c4cdd68988a37e46af8881affe49802 +size 32555 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png index f9cdf0cd9c..b61de7b17c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88a68971b697354a92f04de8b73734d1c4ac10792b212dd34d16e16f8dde5ad5 -size 36572 +oid sha256:3e5ee6f7d37a16a3472eb2a92d611afe270852131f2f330bb3f5ee7db06c1453 +size 34804 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..85b1b93a66 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:333798d610045630944e53c4c43bc401781cc5499ad8c085037239a93de6eccc +size 36950 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithQueryNoBackButton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithQueryNoBackButton_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..13e9fa77a0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithQueryNoBackButton_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79739bfd4bd1970e3ea7d5f938c6303f2aae7a4e332651651c26481609d166d4 +size 7655