diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3408f138b..fdcbd7047a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.8.1 + uses: gradle/gradle-build-action@v2.9.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug APK @@ -55,7 +55,7 @@ jobs: name: elementx-debug path: | app/build/outputs/apk/debug/*.apk - - uses: rnkdsh/action-upload-diawi@v1.5.2 + - uses: rnkdsh/action-upload-diawi@v1.5.3 id: diawi # Do not fail the whole build if Diawi upload fails continue-on-error: true diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index 8b78a3c447..b433ebe459 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -40,7 +40,7 @@ jobs: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} - - uses: mobile-dev-inc/action-maestro-cloud@v1.5.0 + - uses: mobile-dev-inc/action-maestro-cloud@v1.6.0 with: api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} # Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android): diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index 295cd9f1c6..fcde864311 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -62,7 +62,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.8.1 + uses: gradle/gradle-build-action@v2.9.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Dependency analysis diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 6e3efd2ee7..1b880a68fd 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -40,7 +40,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.8.1 + uses: gradle/gradle-build-action@v2.9.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run code quality check suite diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index 8fadf6b352..829f6e1a46 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -24,7 +24,7 @@ jobs: java-version: '17' # Add gradle cache, this should speed up the process - name: Configure gradle - uses: gradle/gradle-build-action@v2.8.1 + uses: gradle/gradle-build-action@v2.9.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Record screenshots diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bce39b7962..bd6179b82e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.8.1 + uses: gradle/gradle-build-action@v2.9.0 - name: Create app bundle env: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 1619e8524b..3250214e22 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -32,7 +32,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.8.1 + uses: gradle/gradle-build-action@v2.9.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: 🔊 Publish results to Sonar diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c07ceedf00..52c6656980 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,7 +44,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.8.1 + uses: gradle/gradle-build-action@v2.9.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} diff --git a/.idea/dictionaries/shared.xml b/.idea/dictionaries/shared.xml index 2fc10f455b..c3aa70ef23 100644 --- a/.idea/dictionaries/shared.xml +++ b/.idea/dictionaries/shared.xml @@ -4,6 +4,7 @@ backstack ftue homeserver + konsist kover measurables onboarding diff --git a/.maestro/tests/account/changeServer.yaml b/.maestro/tests/account/changeServer.yaml index c6b092e018..b56b59e2f1 100644 --- a/.maestro/tests/account/changeServer.yaml +++ b/.maestro/tests/account/changeServer.yaml @@ -10,9 +10,9 @@ appId: ${APP_ID} - tapOn: id: "change_server-server" # Test server that does not support sliding sync. -- inputText: "gnuradio" +- inputText: "https://kieranml.ems-support.element.dev" - hideKeyboard -- tapOn: "gnuradio.org" +- tapOn: "kieranml.ems-support.element.dev" - extendedWaitUntil: visible: "This server currently doesn’t support sliding sync." timeout: 10000 diff --git a/CHANGES.md b/CHANGES.md index 96fc743ee3..796c0d783c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,24 @@ +Changes in Element X v0.2.4 (2023-10-12) +======================================== + +Features ✨ +---------- + - [Rich text editor] Add full screen mode ([#1447](https://github.com/vector-im/element-x-android/issues/1447)) + - Improve rendering of m.emote. ([#1497](https://github.com/vector-im/element-x-android/issues/1497)) + - Improve deleted session behavior. ([#1520](https://github.com/vector-im/element-x-android/issues/1520)) + +Bugfixes 🐛 +---------- + - WebP images can't be sent as media. ([#1483](https://github.com/vector-im/element-x-android/issues/1483)) + - Fix back button not working in bottom sheets. ([#1517](https://github.com/vector-im/element-x-android/issues/1517)) + - Render body of unknown msgtype in the timeline and in the room list ([#1539](https://github.com/vector-im/element-x-android/issues/1539)) + +Other changes +------------- + - Room : makes subscribeToSync/unsubscribeFromSync suspendable. ([#1457](https://github.com/vector-im/element-x-android/issues/1457)) + - Add some Konsist tests. ([#1526](https://github.com/vector-im/element-x-android/issues/1526)) + + Changes in Element X v0.2.3 (2023-09-27) ======================================== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1545c312d..3a729e7992 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,6 +18,7 @@ * [knit](#knit) * [lint](#lint) * [Unit tests](#unit-tests) + * [konsist](#konsist) * [Tests](#tests) * [Accessibility](#accessibility) * [Jetpack Compose](#jetpack-compose) @@ -156,6 +157,10 @@ Make sure the following commands execute without any error: ./gradlew test +#### konsist + +[konsist](https://github.com/LemonAppDev/konsist) is setup in the project to check that the architecture and the naming rules are followed. Konsist tests are classical Unit tests. + ### Tests Element X is currently supported on Android Marshmallow (API 23+): please test your change on an Android device (or Android emulator) running with API 23. Many issues can happen (including crashes) on older devices. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 839a5095dd..5286bde045 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -230,6 +230,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(libs.test.konsist) ksp(libs.showkase.processor) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2b77c7076e..10300ae00b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,14 +20,16 @@ + - + diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index e94a69a365..62af9cf4b2 100644 Binary files a/app/src/main/ic_launcher-playstore.png and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index 2ebfd2ad12..3b35a0ebf4 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -34,7 +34,7 @@ import com.bumble.appyx.core.integrationpoint.NodeComponentActivity import com.bumble.appyx.core.plugin.NodeReadyObserver import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag -import io.element.android.libraries.designsystem.utils.LocalSnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher import io.element.android.libraries.theme.ElementTheme import io.element.android.x.di.AppBindings import io.element.android.x.intent.SafeUriHandler diff --git a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt index 914be65946..185b3834c5 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt @@ -18,7 +18,7 @@ package io.element.android.x.di import com.squareup.anvil.annotations.ContributesTo import io.element.android.features.rageshake.api.reporter.BugReporter -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.tracing.TracingService diff --git a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt index f8ab5532b0..17ba415762 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt @@ -28,7 +28,7 @@ import io.element.android.features.messages.impl.timeline.components.customreact import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.DefaultPreferences diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index f08c673055..793d5ca60d 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp index fff7bbbfd7..d1ff05833e 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index 00b5a99258..78a93b86f1 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 9965e382c1..e8f321ff17 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp index f248938976..f411c1016c 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 347a31e2ea..5380a9e861 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index b1d83c1244..b31de82585 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp index b4090e12b5..5e6654b50c 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 0537ce0a8b..a368522d59 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 60ada6a758..889388eab6 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp index 7311a98da8..9aebd17d21 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index dfae045e0c..af59382417 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index 8282551ecf..97afc844cb 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp index 4efec9cdb2..92e763d12f 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index ecf5481f94..d71ab178fe 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000000..b9f3d03986 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,35 @@ + + + + + + + + + + localhost + 127.0.0.1 + + 10.0.2.2 + + onion + + + + home.arpa + local + test + + home + lan + localdomain + + + + + + + + + + diff --git a/app/src/test/kotlin/io/element/android/app/KonsistTest.kt b/app/src/test/kotlin/io/element/android/app/KonsistTest.kt new file mode 100644 index 0000000000..25367e8c5a --- /dev/null +++ b/app/src/test/kotlin/io/element/android/app/KonsistTest.kt @@ -0,0 +1,139 @@ +/* + * 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.app + +import androidx.compose.runtime.Composable +import com.lemonappdev.konsist.api.KoModifier +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.ext.list.constructors +import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutModifier +import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutOverrideModifier +import com.lemonappdev.konsist.api.ext.list.parameters +import com.lemonappdev.konsist.api.ext.list.properties +import com.lemonappdev.konsist.api.ext.list.withAllAnnotationsOf +import com.lemonappdev.konsist.api.ext.list.withAllParentsOf +import com.lemonappdev.konsist.api.ext.list.withNameEndingWith +import com.lemonappdev.konsist.api.ext.list.withReturnType +import com.lemonappdev.konsist.api.ext.list.withTopLevel +import com.lemonappdev.konsist.api.ext.list.withoutName +import com.lemonappdev.konsist.api.ext.list.withoutNameEndingWith +import com.lemonappdev.konsist.api.verify.assertFalse +import com.lemonappdev.konsist.api.verify.assertTrue +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import org.junit.Test + +class KonsistTest { + + @Test + fun `Classes extending 'Presenter' should have 'Presenter' suffix`() { + Konsist.scopeFromProject() + .classes() + .withAllParentsOf(Presenter::class) + .assertTrue { + it.name.endsWith("Presenter") + } + } + + @Test + fun `Functions with '@PreviewsDayNight' annotation should have 'Preview' suffix`() { + Konsist + .scopeFromProject() + .functions() + .withAllAnnotationsOf(PreviewsDayNight::class) + .assertTrue { + it.hasNameEndingWith("Preview") && + it.hasNameEndingWith("LightPreview").not() && + it.hasNameEndingWith("DarkPreview").not() + } + } + + @Test + fun `Top level function with '@Composable' annotation starting with a upper case should be placed in a file with the same name`() { + Konsist + .scopeFromProject() + .functions() + .withTopLevel() + .withoutModifier(KoModifier.PRIVATE) + .withoutNameEndingWith("Preview") + .withAllAnnotationsOf(Composable::class) + .withoutName( + // Add some exceptions... + "OutlinedButton", + "TextButton", + "SimpleAlertDialogContent", + ) + .assertTrue( + additionalMessage = + """ + Please check the filename. It should match the top level Composable function. If the filename is correct: + - consider making the Composable private or moving it to its own file + - at last resort, you can add an exception in the Konsist test + """.trimIndent() + ) { + if (it.name.first().isLowerCase()) { + true + } else { + val fileName = it.containingFile.name.removeSuffix(".kt") + fileName == it.name + } + } + } + + @Test + fun `Data class state MUST not have default value`() { + Konsist + .scopeFromProject() + .classes() + .withNameEndingWith("State") + .withoutName( + "CameraPositionState", + ) + .constructors + .parameters + .assertTrue { parameterDeclaration -> + parameterDeclaration.defaultValue == null && + // Using parameterDeclaration.defaultValue == null is not enough apparently, + // Also check that the text does not contain an equal sign + parameterDeclaration.text.contains("=").not() + } + } + + @Test + fun `Function which creates Presenter in test MUST be named 'createPresenterName'`() { + Konsist + .scopeFromTest() + .functions() + .withReturnType { it.name.endsWith("Presenter") } + .withoutOverrideModifier() + .assertTrue { functionDeclaration -> + functionDeclaration.name == "create${functionDeclaration.returnType?.name}" + } + } + + @Test + fun `no field should have 'm' prefix`() { + Konsist + .scopeFromProject() + .classes() + .properties() + .assertFalse { + val secondCharacterIsUppercase = it.name.getOrNull(1)?.isUpperCase() ?: false + it.name.startsWith('m') && secondCharacterIsUppercase + } + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt index e55f059d14..6bf7687fef 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt @@ -16,8 +16,8 @@ package io.element.android.appnav -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher -import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationFlowState diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index d1a953ba09..72fd2461d2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -26,8 +26,10 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.composable.PermanentChild import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins @@ -56,7 +58,7 @@ import io.element.android.libraries.architecture.animation.rememberDefaultTransi import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.waitForChildAttached import io.element.android.libraries.deeplink.DeeplinkData -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.MAIN_SPACE @@ -95,6 +97,10 @@ class LoggedInFlowNode @AssistedInject constructor( initialElement = NavTarget.RoomList, savedStateMap = buildContext.savedStateMap, ), + permanentNavModel = PermanentNavModel( + NavTarget.Permanent, + savedStateMap = buildContext.savedStateMap, + ), buildContext = buildContext, plugins = plugins ) { @@ -328,7 +334,7 @@ class LoggedInFlowNode @AssistedInject constructor( val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState() if (!isFtueDisplayed) { - PermanentChild(navTarget = NavTarget.Permanent) + PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.Permanent) } } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 94f344be7e..df98998ba5 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -45,6 +45,7 @@ import io.element.android.appnav.root.RootView import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.api.oidc.OidcActionFlow import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint +import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -54,6 +55,7 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.sessionstorage.api.LoggedInState import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -69,6 +71,7 @@ class RootFlowNode @AssistedInject constructor( private val matrixClientsHolder: MatrixClientsHolder, private val presenter: RootPresenter, private val bugReportEntryPoint: BugReportEntryPoint, + private val signedOutEntryPoint: SignedOutEntryPoint, private val intentResolver: IntentResolver, private val oidcActionFlow: OidcActionFlow, ) : BackstackNode( @@ -97,13 +100,20 @@ class RootFlowNode @AssistedInject constructor( .distinctUntilChanged() .onEach { navState -> Timber.v("navState=$navState") - if (navState.isLoggedIn) { - tryToRestoreLatestSession( - onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) }, - onFailure = { switchToNotLoggedInFlow() } - ) - } else { - switchToNotLoggedInFlow() + when (navState.loggedInState) { + is LoggedInState.LoggedIn -> { + if (navState.loggedInState.isTokenValid) { + tryToRestoreLatestSession( + onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) }, + onFailure = { switchToNotLoggedInFlow() } + ) + } else { + switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId)) + } + } + LoggedInState.NotLoggedIn -> { + switchToNotLoggedInFlow() + } } } .launchIn(lifecycleScope) @@ -118,6 +128,10 @@ class RootFlowNode @AssistedInject constructor( backstack.safeRoot(NavTarget.NotLoggedInFlow) } + private fun switchToSignedOutFlow(sessionId: SessionId) { + backstack.safeRoot(NavTarget.SignedOutFlow(sessionId)) + } + private suspend fun restoreSessionIfNeeded( sessionId: SessionId, onFailure: () -> Unit = {}, @@ -179,6 +193,11 @@ class RootFlowNode @AssistedInject constructor( val navId: Int ) : NavTarget + @Parcelize + data class SignedOutFlow( + val sessionId: SessionId + ) : NavTarget + @Parcelize data object BugReport : NavTarget } @@ -198,6 +217,15 @@ class RootFlowNode @AssistedInject constructor( createNode(buildContext, plugins = listOf(inputs, callback)) } NavTarget.NotLoggedInFlow -> createNode(buildContext) + is NavTarget.SignedOutFlow -> { + signedOutEntryPoint.nodeBuilder(this, buildContext) + .params( + SignedOutEntryPoint.Params( + sessionId = navTarget.sessionId + ) + ) + .build() + } NavTarget.SplashScreen -> splashNode(buildContext) NavTarget.BugReport -> { val callback = object : BugReportEntryPoint.Callback { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt index 34c459f979..8ad9beba61 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt @@ -46,6 +46,7 @@ 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.RoomMembershipObserver import io.element.android.services.appnavstate.api.AppNavigationStateService +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -60,6 +61,7 @@ class RoomLoadedFlowNode @AssistedInject constructor( private val messagesEntryPoint: MessagesEntryPoint, private val roomDetailsEntryPoint: RoomDetailsEntryPoint, private val appNavigationStateService: AppNavigationStateService, + private val appCoroutineScope: CoroutineScope, roomComponentFactory: RoomComponentFactory, roomMembershipObserver: RoomMembershipObserver, ) : BackstackNode( @@ -91,6 +93,16 @@ class RoomLoadedFlowNode @AssistedInject constructor( appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId) fetchRoomMembers() }, + onResume = { + appCoroutineScope.launch { + inputs.room.subscribeToSync() + } + }, + onPause = { + appCoroutineScope.launch { + inputs.room.unsubscribeFromSync() + } + }, onDestroy = { Timber.v("OnDestroy") appNavigationStateService.onLeavingRoom(id) @@ -162,9 +174,7 @@ class RoomLoadedFlowNode @AssistedInject constructor( // because this node enters 'onDestroy' before his children, so it can leads to // using the room in a child node where it's already closed. DisposableEffect(Unit) { - inputs.room.subscribeToSync() onDispose { - inputs.room.unsubscribeFromSync() if (lifecycle.currentState == Lifecycle.State.DESTROYED) { inputs.room.destroy() } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt index ed3ac15972..09f9767f62 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt @@ -16,6 +16,8 @@ package io.element.android.appnav.root +import io.element.android.libraries.sessionstorage.api.LoggedInState + /** * [RootNavState] produced by [RootNavStateFlowFactory]. */ @@ -26,7 +28,7 @@ data class RootNavState( */ val cacheIndex: Int, /** - * true if we are currently loggedIn. + * LoggedInState. */ - val isLoggedIn: Boolean + val loggedInState: LoggedInState, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt index 0e8d93b0c9..e6a46f5950 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt @@ -22,9 +22,9 @@ import io.element.android.appnav.di.MatrixClientsHolder import io.element.android.features.login.api.LoginUserStory import io.element.android.features.preferences.api.CacheService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.sessionstorage.api.LoggedInState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach import javax.inject.Inject @@ -47,9 +47,14 @@ class RootNavStateFlowFactory @Inject constructor( fun create(savedStateMap: SavedStateMap?): Flow { return combine( cacheIndexFlow(savedStateMap), - isUserLoggedInFlow(), - ) { cacheIndex, isLoggedIn -> - RootNavState(cacheIndex = cacheIndex, isLoggedIn = isLoggedIn) + authenticationService.loggedInStateFlow(), + loginUserStory.loginFlowIsDone, + ) { cacheIndex, loggedInState, loginFlowIsDone -> + if (loginFlowIsDone) { + RootNavState(cacheIndex = cacheIndex, loggedInState = loggedInState) + } else { + RootNavState(cacheIndex = cacheIndex, loggedInState = LoggedInState.NotLoggedIn) + } } } @@ -72,16 +77,6 @@ class RootNavStateFlowFactory @Inject constructor( } } - private fun isUserLoggedInFlow(): Flow { - return combine( - authenticationService.isLoggedIn(), - loginUserStory.loginFlowIsDone - ) { isLoggedIn, loginFlowIsDone -> - isLoggedIn && loginFlowIsDone - } - .distinctUntilChanged() - } - /** * @return a flow of integer that increments the value by one each time a new element is emitted upstream. */ diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt index a3bf1359f9..a25e4d8134 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt @@ -35,6 +35,8 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.services.appnavstate.test.FakeAppNavigationStateService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -85,6 +87,7 @@ class RoomFlowNodeTest { plugins: List, messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(), roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(), + coroutineScope: CoroutineScope, ) = RoomLoadedFlowNode( buildContext = BuildContext.root(savedStateMap = null), plugins = plugins, @@ -92,16 +95,21 @@ class RoomFlowNodeTest { roomDetailsEntryPoint = roomDetailsEntryPoint, appNavigationStateService = FakeAppNavigationStateService(), roomMembershipObserver = RoomMembershipObserver(), + appCoroutineScope = coroutineScope, roomComponentFactory = FakeRoomComponentFactory(), ) @Test - fun `given a room flow node when initialized then it loads messages entry point`() { + fun `given a room flow node when initialized then it loads messages entry point`() = runTest { // GIVEN val room = FakeMatrixRoom() val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val inputs = RoomLoadedFlowNode.Inputs(room) - val roomFlowNode = aRoomFlowNode(listOf(inputs), fakeMessagesEntryPoint) + val roomFlowNode = aRoomFlowNode( + plugins = listOf(inputs), + messagesEntryPoint = fakeMessagesEntryPoint, + coroutineScope = this + ) // WHEN val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() @@ -113,13 +121,18 @@ class RoomFlowNodeTest { } @Test - fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() { + fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest { // GIVEN val room = FakeMatrixRoom() val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() val inputs = RoomLoadedFlowNode.Inputs(room) - val roomFlowNode = aRoomFlowNode(listOf(inputs), fakeMessagesEntryPoint, fakeRoomDetailsEntryPoint) + val roomFlowNode = aRoomFlowNode( + plugins = listOf(inputs), + messagesEntryPoint = fakeMessagesEntryPoint, + roomDetailsEntryPoint = fakeRoomDetailsEntryPoint, + coroutineScope = this + ) val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() // WHEN fakeMessagesEntryPoint.callback?.onRoomDetailsClicked() 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 6771718d81..5b382d1dc5 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt @@ -42,7 +42,7 @@ class RootPresenterTest { @Test fun `present - initial state`() = runTest { - val presenter = createPresenter() + val presenter = createRootPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -54,7 +54,7 @@ class RootPresenterTest { @Test fun `present - passes app error state`() = runTest { - val presenter = createPresenter( + val presenter = createRootPresenter( appErrorService = DefaultAppErrorStateService().apply { showError("Bad news", "Something bad happened") } @@ -75,7 +75,7 @@ class RootPresenterTest { } } - private fun createPresenter( + private fun createRootPresenter( appErrorService: AppErrorStateService = DefaultAppErrorStateService() ): RootPresenter { val crashDataStore = FakeCrashDataStore() diff --git a/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixClientsHolderTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixClientsHolderTest.kt new file mode 100644 index 0000000000..6dce8a6310 --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixClientsHolderTest.kt @@ -0,0 +1,93 @@ +/* + * 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.appnav.di + +import com.bumble.appyx.core.state.MutableSavedStateMapImpl +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MatrixClientsHolderTest { + @Test + fun `test getOrNull`() { + val fakeAuthenticationService = FakeAuthenticationService() + val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) + assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull() + } + + @Test + fun `test getOrRestore`() = runTest { + val fakeAuthenticationService = FakeAuthenticationService() + val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) + val fakeMatrixClient = FakeMatrixClient() + fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) + assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull() + assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) + // Do it again to it the cache + assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) + assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) + } + + @Test + fun `test remove`() = runTest { + val fakeAuthenticationService = FakeAuthenticationService() + val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) + val fakeMatrixClient = FakeMatrixClient() + fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) + assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) + assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) + // Remove + matrixClientsHolder.remove(A_SESSION_ID) + assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull() + } + + @Test + fun `test remove all`() = runTest { + val fakeAuthenticationService = FakeAuthenticationService() + val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) + val fakeMatrixClient = FakeMatrixClient() + fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) + assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) + assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) + // Remove all + matrixClientsHolder.removeAll() + assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull() + } + + @Test + fun `test save and restore`() = runTest { + val fakeAuthenticationService = FakeAuthenticationService() + val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService) + val fakeMatrixClient = FakeMatrixClient() + fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) + matrixClientsHolder.getOrRestore(A_SESSION_ID) + val savedStateMap = MutableSavedStateMapImpl { true } + matrixClientsHolder.saveIntoSavedState(savedStateMap) + assertThat(savedStateMap.size).isEqualTo(1) + // Test Restore with non-empty map + matrixClientsHolder.restoreWithSavedState(savedStateMap) + // Empty the map + matrixClientsHolder.removeAll() + assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull() + // Restore again + matrixClientsHolder.restoreWithSavedState(savedStateMap) + assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) + } +} diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt index efada670b0..fcfba8f7a1 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -42,7 +42,7 @@ class LoggedInPresenterTest { @Test fun `present - initial state`() = runTest { - val presenter = createPresenter() + val presenter = createLoggedInPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -54,7 +54,7 @@ class LoggedInPresenterTest { @Test fun `present - show sync spinner`() = runTest { val roomListService = FakeRoomListService() - val presenter = createPresenter(roomListService, NetworkStatus.Online) + val presenter = createLoggedInPresenter(roomListService, NetworkStatus.Online) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -66,7 +66,7 @@ class LoggedInPresenterTest { } } - private fun createPresenter( + private fun createLoggedInPresenter( roomListService: RoomListService = FakeRoomListService(), networkStatus: NetworkStatus = NetworkStatus.Offline ): LoggedInPresenter { diff --git a/build.gradle.kts b/build.gradle.kts index a3cec94af9..f08c023b1d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,7 +45,7 @@ plugins { } tasks.register("clean").configure { - delete(rootProject.buildDir) + delete(rootProject.layout.buildDirectory) } allprojects { @@ -86,7 +86,7 @@ allprojects { reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE) } filter { - exclude { element -> element.file.path.contains("$buildDir/generated/") } + exclude { element -> element.file.path.contains("${layout.buildDirectory.asFile.get()}/generated/") } } } // Dependency check @@ -176,10 +176,13 @@ koverMerged { "*_ModuleKt", "anvil.hint.binding.io.element.*", "anvil.hint.merge.*", + "anvil.hint.multibinding.io.element.*", "anvil.module.*", "com.airbnb.android.showkase*", "io.element.android.libraries.designsystem.showkase.*", + "io.element.android.x.di.DaggerAppComponent*", "*_Factory", + "*_Factory_Impl", "*_Factory$*", "*_Module", "*_Module$*", @@ -228,11 +231,11 @@ koverMerged { name = "Global minimum code coverage." target = kotlinx.kover.api.VerificationTarget.ALL bound { - minValue = 55 + minValue = 60 // Setting a max value, so that if coverage is bigger, it means that we have to change minValue. // For instance if we have minValue = 20 and maxValue = 30, and current code coverage is now 31.32%, update // minValue to 25 and maxValue to 35. - maxValue = 65 + maxValue = 70 counter = kotlinx.kover.api.CounterType.INSTRUCTION valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE } @@ -354,7 +357,7 @@ subprojects { subprojects { tasks.withType() { doLast { - fileTree(buildDir).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file -> + fileTree(layout.buildDirectory).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file -> ReplaceRegExp().apply { setMatch("^public fun Showkase.getMetadata") setReplace("@Suppress(\"DEPRECATION\") public fun Showkase.getMetadata") diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md index 8a587b5a08..7af9207365 100644 --- a/docs/_developer_onboarding.md +++ b/docs/_developer_onboarding.md @@ -120,18 +120,9 @@ You can also have access to the aars through the [release](https://github.com/ma If you need to locally build the sdk-android you can use the [build](https://github.com/matrix-org/matrix-rust-components-kotlin/blob/main/scripts/build.sh) script. -For this, you first need to ensure to setup : +For this please check the [prerequisites](https://github.com/matrix-org/matrix-rust-components-kotlin/blob/main/README.md#prerequisites) from the repo. -- rust environment (check https://rust-lang.github.io/rustup/ if needed) -- cargo-ndk < 2.12.0 -```shell -cargo install cargo-ndk --version 2.11.0 -``` -- android targets -```shell -rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android -``` -- checkout both [matrix-rust-sdk](https://github.com/matrix-org/matrix-rust-sdk) and [matrix-rust-components-kotlin](https://github.com/matrix-org/matrix-rust-components-kotlin) repositories +Checkout both [matrix-rust-sdk](https://github.com/matrix-org/matrix-rust-sdk) and [matrix-rust-components-kotlin](https://github.com/matrix-org/matrix-rust-components-kotlin) repositories ```shell git clone git@github.com:matrix-org/matrix-rust-sdk.git git clone git@github.com:matrix-org/matrix-rust-components-kotlin.git @@ -151,6 +142,11 @@ So for example to build the sdk against aarch64-linux-android target and copy th ./scripts/build.sh -p [YOUR MATRIX RUST SDK PATH] -t aarch64-linux-android -o [YOUR element-x-android PATH]/libraries/rustsdk/matrix-rust-sdk.aar ``` +Troubleshooting: + - You may need to set `ANDROID_NDK_HOME` e.g `export ANDROID_NDK_HOME=~/Library/Android/sdk/ndk`. + - If you get the error `thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', .cargo/registry/src/index.crates.io-6f17d22bba15001f/cargo-ndk-2.11.0/src/cli.rs:345:18` try updating your Cargo NDK version. In this case, 2.11.0 is too old so `cargo install cargo-ndk` to install a newer version. + - If you get the error `Unsupported class file major version 64` try changing your JVM version. In this case, Java 20 is not supported in Gradle yet, so downgrade to an earlier version (Java 17 worked in this case). + Finally let the `matrix/impl` module use this aar by changing the dependencies from `libs.matrix.sdk` to `projects.libraries.rustsdk`: ```groovy @@ -280,11 +276,12 @@ Follow these steps to install and configure the plugin and templates: 1. Install the AS plugin for generating modules : [Generate Module from Template](https://plugins.jetbrains.com/plugin/13586-generate-module-from-template) -2. Import file templates in AS : +2. Run the script `tools/templates/generate_templates.sh` to generate the template zip file +3. Import file templates in AS : - Navigate to File/Manage IDE Settings/Import Settings - - Pick the `tools/templates/file_templates.zip` files + - Pick the `tmp/file_templates.zip` files - Click on OK -3. Configure generate-module-from-template plugin : +4. Configure generate-module-from-template plugin : - Navigate to AS/Settings/Tools/Module Template Settings - Click on + / Import From File - Pick the `tools/templates/FeatureModule.json` diff --git a/docs/debug_proxying.md b/docs/debug_proxying.md new file mode 100644 index 0000000000..77c109461b --- /dev/null +++ b/docs/debug_proxying.md @@ -0,0 +1,22 @@ +# Setup a debug mitm proxy to inspect all the app's network traffic + +1) Install mitmproxy: `brew install mitmproxy`. + 1) Launch `mitmweb` from a terminal. It will pop up mitmproxy's web interface in a web browser. +1) Configure Android Emulator. + 1) Launch your android emulator. + 1) Open its settings page and go to Settings -> Proxy (nb this tab isn't visible when running the emu inside the Android Studio window, you need to set it so it runs in its own window). + 1) Disable "Use Android Studio HTTP proxy settings" and pick "Manual proxy configuration". + 1) Set `127.0.0.1` as "Host name" and `8080` as "Port number". + 1) Click "Apply" and verify that "Proxy status" is "Success" and close the settings window. + Screenshot 2023-10-04 at 14 48 47 +1) Install the mitmproxy CA cert (this is needed to see traffic from java/kotlin code, it's not needed for traffic coming from native code e.g. the matrix-rust-sdk). + 1) Open the emulator Chrome browser app + 1) Go to the url `mitm.it` + 1) Follow the instructions to install the CA cert on Android devices. + Screenshot 2023-10-04 at 14 51 27 +1) Slightly modify the Element X app source code. + 1) Go to the `RustMatrixClientFactory.create()` method. + 1) Add `.disableSslVerification()` in the `ClientBuilder` method chain. +1) Build and run the Element X app. +1) Enjoy, you will see all the traffic in mitmproxy's web interface. + Screenshot 2023-10-04 at 14 50 03 diff --git a/fastlane/metadata/android/en-US/changelogs/40002040.txt b/fastlane/metadata/android/en-US/changelogs/40002040.txt new file mode 100644 index 0000000000..ff4f86ce7e --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40002040.txt @@ -0,0 +1,2 @@ +Main changes in this version: bugfixes. +Full changelog: https://github.com/vector-im/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png index 37975de877..eb6cabd122 100644 Binary files a/fastlane/metadata/android/en-US/images/featureGraphic.png and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png index cfe22b43cd..a3107af4b1 100644 Binary files a/fastlane/metadata/android/en-US/images/icon.png and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index b1668d1b2d..3214245ded 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png index 30d6a71a01..652d91fa39 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png index c62d890ca2..e91fb3c260 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png index 0a612798a6..9e9f87ef0f 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt index 4cdb14cba9..7aa007c9e5 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt @@ -44,8 +44,8 @@ import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.features.analytics.api.Config import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule -import io.element.android.libraries.designsystem.atomic.molecules.InfoListItem -import io.element.android.libraries.designsystem.atomic.molecules.InfoListOrganism +import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem +import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview diff --git a/features/analytics/impl/src/main/res/values-zh-rTW/translations.xml b/features/analytics/impl/src/main/res/values-zh-rTW/translations.xml index 996e5f5f3b..433c5b051e 100644 --- a/features/analytics/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/analytics/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,7 +1,7 @@ "我們不會紀錄或剖繪您的個人資料" - "分享匿名的使用數據以協助我們釐清問題" + "分享匿名的使用數據以協助我們釐清問題。" "您可以到%1$s閱讀我們的條款。" "這裡" "您可以在任何時候關閉它" diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt b/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt index b3e2f9227a..b903b437d8 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt @@ -54,21 +54,50 @@ class CallIntentDataParser @Inject constructor() { } /** - * Ensure the uri has the following parameters and value: + * Ensure the uri has the following parameters and value in the fragment: * - appPrompt=false * - confineToRoom=true * to ensure that the rendering will bo correct on the embedded Webview. */ private fun Uri.withCustomParameters(): String { val builder = buildUpon() + // Remove the existing query parameters builder.clearQuery() queryParameterNames.forEach { if (it == APP_PROMPT_PARAMETER || it == CONFINE_TO_ROOM_PARAMETER) return@forEach builder.appendQueryParameter(it, getQueryParameter(it)) } - builder.appendQueryParameter(APP_PROMPT_PARAMETER, "false") - builder.appendQueryParameter(CONFINE_TO_ROOM_PARAMETER, "true") - return builder.build().toString() + // Remove the existing fragment parameters, and build the new fragment + val currentFragment = fragment ?: "" + // Reset the current fragment + builder.fragment("") + val queryFragmentPosition = currentFragment.lastIndexOf("?") + val newFragment = if (queryFragmentPosition == -1) { + // No existing query, build it. + "$currentFragment?$APP_PROMPT_PARAMETER=false&$CONFINE_TO_ROOM_PARAMETER=true" + } else { + buildString { + append(currentFragment.substring(0, queryFragmentPosition + 1)) + val queryFragment = currentFragment.substring(queryFragmentPosition + 1) + // Replace the existing parameters + val newQueryFragment = queryFragment + .replace("$APP_PROMPT_PARAMETER=true", "$APP_PROMPT_PARAMETER=false") + .replace("$CONFINE_TO_ROOM_PARAMETER=false", "$CONFINE_TO_ROOM_PARAMETER=true") + append(newQueryFragment) + // Ensure the parameters are there + if (!newQueryFragment.contains("$APP_PROMPT_PARAMETER=false")) { + if (newQueryFragment.isNotEmpty()) { + append("&") + } + append("$APP_PROMPT_PARAMETER=false") + } + if (!newQueryFragment.contains("$CONFINE_TO_ROOM_PARAMETER=true")) { + append("&$CONFINE_TO_ROOM_PARAMETER=true") + } + } + } + // We do not want to encode the Fragment part, so append it manually + return builder.build().toString() + "#" + newFragment } private const val APP_PROMPT_PARAMETER = "appPrompt" diff --git a/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt b/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt index aee97ed982..f23a5fb43c 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt +++ b/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt @@ -35,92 +35,51 @@ class CallIntentDataParserTests { @Test fun `empty data returns null`() { - val url = "" - assertThat(callIntentDataParser.parse(url)).isNull() + doTest("", null) } @Test fun `invalid data returns null`() { - val url = "!" - assertThat(callIntentDataParser.parse(url)).isNull() + doTest("!", null) } @Test fun `data with no scheme returns null`() { - val url = "test" - assertThat(callIntentDataParser.parse(url)).isNull() + doTest("test", null) } @Test fun `Element Call http urls returns null`() { - val httpBaseUrl = "http://call.element.io" - val httpCallUrl = "http://call.element.io/some-actual-call?with=parameters" - assertThat(callIntentDataParser.parse(httpBaseUrl)).isNull() - assertThat(callIntentDataParser.parse(httpCallUrl)).isNull() + doTest("http://call.element.io", null) + doTest("http://call.element.io/some-actual-call?with=parameters", null) } @Test fun `Element Call urls will be returned as is`() { - val httpsBaseUrl = "https://call.element.io" - val httpsCallUrl = VALID_CALL_URL_WITH_PARAM - assertThat(callIntentDataParser.parse(httpsBaseUrl)).isEqualTo("$httpsBaseUrl?$EXTRA_PARAMS") - assertThat(callIntentDataParser.parse(httpsCallUrl)).isEqualTo("$httpsCallUrl&$EXTRA_PARAMS") + doTest( + url = "https://call.element.io", + expectedResult = "https://call.element.io#?$EXTRA_PARAMS" + ) + } + + @Test + fun `Element Call url with url param gets url extracted`() { + doTest( + url = VALID_CALL_URL_WITH_PARAM, + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" + ) } @Test fun `HTTP and HTTPS urls that don't come from EC return null`() { - val httpBaseUrl = "http://app.element.io" - val httpsBaseUrl = "https://app.element.io" - val httpInvalidUrl = "http://" - val httpsInvalidUrl = "http://" - assertThat(callIntentDataParser.parse(httpBaseUrl)).isNull() - assertThat(callIntentDataParser.parse(httpsBaseUrl)).isNull() - assertThat(callIntentDataParser.parse(httpInvalidUrl)).isNull() - assertThat(callIntentDataParser.parse(httpsInvalidUrl)).isNull() + doTest("http://app.element.io", null) + doTest("https://app.element.io", null, testEmbedded = false) + doTest("http://", null) + doTest("https://", null) } @Test - fun `element scheme with call host and url with http will returns null`() { - val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "element://call?url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `element scheme with call host and url param gets url extracted`() { - val embeddedUrl = VALID_CALL_URL_WITH_PARAM - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "element://call?url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS") - } - - @Test - fun `element scheme 2 with url param with http returns null`() { - val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "io.element.call:/?url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `element scheme 2 with url param gets url extracted`() { - val embeddedUrl = VALID_CALL_URL_WITH_PARAM - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "io.element.call:/?url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS") - } - - @Test - fun `element scheme with call host and no url param returns null`() { - val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "element://call?no-url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `element scheme 2 with no url returns null`() { + fun `Element Call url with no url returns null`() { val embeddedUrl = VALID_CALL_URL_WITH_PARAM val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") val url = "io.element.call:/?no_url=$encodedUrl" @@ -142,7 +101,7 @@ class CallIntentDataParserTests { } @Test - fun `element scheme 2 with no data returns null`() { + fun `Element Call url with no data returns null`() { val url = "io.element.call:/?url=" assertThat(callIntentDataParser.parse(url)).isNull() } @@ -156,29 +115,108 @@ class CallIntentDataParserTests { } @Test - fun `element scheme 2 with url extra param appPrompt gets url extracted`() { - val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}&appPrompt=true" - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "io.element.call:/?url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS") + fun `Element Call url with url extra param appPrompt gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}&appPrompt=true", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" + ) } @Test - fun `element scheme 2 with url extra param confineToRoom gets url extracted`() { - val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}&confineToRoom=false" - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "io.element.call:/?url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS") + fun `Element Call url with url extra param in fragment appPrompt gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#?appPrompt=true", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&confineToRoom=true" + ) } @Test - fun `element scheme 2 with url fragment gets url extracted`() { - val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}#fragment" - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "io.element.call:/?url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS#fragment") + fun `Element Call url with url extra param in fragment appPrompt and other gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#?appPrompt=true&otherParam=maybe", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&otherParam=maybe&confineToRoom=true" + ) } + @Test + fun `Element Call url with url extra param confineToRoom gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}&confineToRoom=false", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" + ) + } + + @Test + fun `Element Call url with url extra param in fragment confineToRoom gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#?confineToRoom=false", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&appPrompt=false" + ) + } + + @Test + fun `Element Call url with url extra param in fragment confineToRoom and more gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#?confineToRoom=false&otherParam=maybe", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&otherParam=maybe&appPrompt=false" + ) + } + + @Test + fun `Element Call url with url fragment gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#fragment", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?$EXTRA_PARAMS" + ) + } + + @Test + fun `Element Call url with url fragment with params gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#fragment?otherParam=maybe", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?otherParam=maybe&$EXTRA_PARAMS" + ) + } + + @Test + fun `Element Call url with url fragment with other params gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#?otherParam=maybe", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?otherParam=maybe&$EXTRA_PARAMS" + ) + } + + @Test + fun `Element Call url with empty fragment`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" + ) + } + + @Test + fun `Element Call url with empty fragment query`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#?", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" + ) + } + + private fun doTest(url: String, expectedResult: String?, testEmbedded: Boolean = true) { + // Test direct parsing + assertThat(callIntentDataParser.parse(url)).isEqualTo(expectedResult) + + if (testEmbedded) { + // Test embedded url, scheme 1 + val encodedUrl = URLEncoder.encode(url, "utf-8") + val urlScheme1 = "element://call?url=$encodedUrl" + assertThat(callIntentDataParser.parse(urlScheme1)).isEqualTo(expectedResult) + + // Test embedded url, scheme 2 + val urlScheme2 = "io.element.call:/?url=$encodedUrl" + assertThat(callIntentDataParser.parse(urlScheme2)).isEqualTo(expectedResult) + } + } companion object { const val VALID_CALL_URL_WITH_PARAM = "https://call.element.io/some-actual-call?with=parameters" 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 aa5ee5f3bd..40a597b649 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 @@ -84,7 +84,7 @@ fun AddPeopleView( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AddPeopleViewTopBar( +private fun AddPeopleViewTopBar( hasSelectedUsers: Boolean, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 6398536e14..8c4de1af33 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -181,7 +181,7 @@ fun ConfigureRoomView( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ConfigureRoomToolbar( +private fun ConfigureRoomToolbar( isNextActionEnabled: Boolean, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, @@ -207,7 +207,7 @@ fun ConfigureRoomToolbar( } @Composable -fun RoomNameWithAvatar( +private fun RoomNameWithAvatar( avatarUri: Uri?, roomName: String, modifier: Modifier = Modifier, @@ -235,7 +235,7 @@ fun RoomNameWithAvatar( } @Composable -fun RoomTopic( +private fun RoomTopic( topic: String, modifier: Modifier = Modifier, onTopicChanged: (String) -> Unit = {}, @@ -254,7 +254,7 @@ fun RoomTopic( } @Composable -fun RoomPrivacyOptions( +private fun RoomPrivacyOptions( selected: RoomPrivacy?, modifier: Modifier = Modifier, onOptionSelected: (RoomPrivacyItem) -> Unit = {}, 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 cf77ebc189..452efa2b85 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 @@ -126,7 +126,7 @@ fun CreateRoomRootView( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun CreateRoomRootViewTopBar( +private fun CreateRoomRootViewTopBar( modifier: Modifier = Modifier, onClosePressed: () -> Unit = {}, ) { @@ -148,7 +148,7 @@ fun CreateRoomRootViewTopBar( } @Composable -fun CreateRoomActionButtonsList( +private fun CreateRoomActionButtonsList( state: CreateRoomRootState, modifier: Modifier = Modifier, onNewRoomClicked: () -> Unit = {}, @@ -169,7 +169,7 @@ fun CreateRoomActionButtonsList( } @Composable -fun CreateRoomActionButton( +private fun CreateRoomActionButton( @DrawableRes iconRes: Int, text: String, modifier: Modifier = Modifier, diff --git a/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml b/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml index 2caa011abc..a078802c36 100644 --- a/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml @@ -4,6 +4,10 @@ "邀請朋友使用 Element" "邀請夥伴" "建立聊天室時發生錯誤" + "聊天室裡的訊息會被加密。聊天室建立後,無法停用加密功能。" + "私密聊天室(僅限邀請)" + "訊息未加密,任何人都可以查看。您可以在之後啟用加密功能。" + "公開聊天室(任何人)" "聊天室名稱" "主題(非必填)" "建立聊天室" diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt index e8b1442c24..cef0529fb8 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt @@ -38,8 +38,8 @@ import androidx.compose.ui.unit.dp import io.element.android.features.ftue.impl.R import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize -import io.element.android.libraries.designsystem.atomic.molecules.InfoListItem -import io.element.android.libraries.designsystem.atomic.molecules.InfoListOrganism +import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem +import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview diff --git a/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml b/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml index b8b510aac5..1edd145776 100644 --- a/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,5 +1,11 @@ - "設定您的帳號" + "這是一次性的程序,感謝您耐心等候。" + "正在設定您的帳號。" + "通話、投票、搜尋等更多功能將在今年登場。" + "在這次的更新,您無法查看聊天室內被加密的歷史訊息。" + "我們很樂意聽取您的意見,請到設定頁面告訴我們您的想法。" "開始吧!" + "我們有些事想告訴您:" + "歡迎使用 %1$s!" diff --git a/features/invitelist/impl/build.gradle.kts b/features/invitelist/impl/build.gradle.kts index d8fb25585f..110eb7946e 100644 --- a/features/invitelist/impl/build.gradle.kts +++ b/features/invitelist/impl/build.gradle.kts @@ -14,8 +14,6 @@ * limitations under the License. */ -// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed -@Suppress("DSL_SCOPE_VIOLATION") plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt index c1e00727f9..43644cd6aa 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt @@ -25,10 +25,10 @@ import kotlinx.collections.immutable.ImmutableList @Immutable data class InviteListState( val inviteList: ImmutableList, - val declineConfirmationDialog: InviteDeclineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden, - val acceptedAction: Async = Async.Uninitialized, - val declinedAction: Async = Async.Uninitialized, - val eventSink: (InviteListEvents) -> Unit = {} + val declineConfirmationDialog: InviteDeclineConfirmationDialog, + val acceptedAction: Async, + val declinedAction: Async, + val eventSink: (InviteListEvents) -> Unit ) sealed interface InviteDeclineConfirmationDialog { diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListStateProvider.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListStateProvider.kt index d4d1f5c166..f187398689 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListStateProvider.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListStateProvider.kt @@ -39,6 +39,10 @@ open class InviteListStateProvider : PreviewParameterProvider { internal fun aInviteListState() = InviteListState( inviteList = aInviteListInviteSummaryList(), + declineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden, + acceptedAction = Async.Uninitialized, + declinedAction = Async.Uninitialized, + eventSink = {}, ) internal fun aInviteListInviteSummaryList(): ImmutableList { diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt index eb6dc312c4..d6d2d2be49 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt @@ -111,7 +111,7 @@ fun InviteListView( @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable -fun InviteListContent( +private fun InviteListContent( state: InviteListState, modifier: Modifier = Modifier, onBackClicked: () -> Unit = {}, diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt index 27d8de0117..b48cea95da 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt @@ -77,7 +77,7 @@ internal fun InviteSummaryRow( } @Composable -internal fun DefaultInviteSummaryRow( +private fun DefaultInviteSummaryRow( invite: InviteListInviteSummary, onAcceptClicked: () -> Unit = {}, onDeclineClicked: () -> Unit = {}, diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt index 3f14833cf0..df6d44df4d 100644 --- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt @@ -19,10 +19,10 @@ package io.element.android.features.leaveroom.api import io.element.android.libraries.matrix.api.core.RoomId data class LeaveRoomState( - val confirmation: Confirmation = Confirmation.Hidden, - val progress: Progress = Progress.Hidden, - val error: Error = Error.Hidden, - val eventSink: (LeaveRoomEvent) -> Unit = {}, + val confirmation: Confirmation, + val progress: Progress, + val error: Error, + val eventSink: (LeaveRoomEvent) -> Unit, ) { sealed interface Confirmation { data object Hidden : Confirmation diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt index e9b08bcd18..f82b58cba3 100644 --- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt @@ -22,32 +22,32 @@ import io.element.android.libraries.matrix.api.core.RoomId class LeaveRoomStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - LeaveRoomState( + aLeaveRoomState( confirmation = LeaveRoomState.Confirmation.Hidden, progress = LeaveRoomState.Progress.Hidden, error = LeaveRoomState.Error.Hidden, ), - LeaveRoomState( + aLeaveRoomState( confirmation = LeaveRoomState.Confirmation.Generic(A_ROOM_ID), progress = LeaveRoomState.Progress.Hidden, error = LeaveRoomState.Error.Hidden, ), - LeaveRoomState( + aLeaveRoomState( confirmation = LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID), progress = LeaveRoomState.Progress.Hidden, error = LeaveRoomState.Error.Hidden, ), - LeaveRoomState( + aLeaveRoomState( confirmation = LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID), progress = LeaveRoomState.Progress.Hidden, error = LeaveRoomState.Error.Hidden, ), - LeaveRoomState( + aLeaveRoomState( confirmation = LeaveRoomState.Confirmation.Hidden, progress = LeaveRoomState.Progress.Shown, error = LeaveRoomState.Error.Hidden, ), - LeaveRoomState( + aLeaveRoomState( confirmation = LeaveRoomState.Confirmation.Hidden, progress = LeaveRoomState.Progress.Hidden, error = LeaveRoomState.Error.Shown, @@ -56,3 +56,14 @@ class LeaveRoomStateProvider : PreviewParameterProvider { } private val A_ROOM_ID = RoomId("!aRoomId:aDomain") + +fun aLeaveRoomState( + confirmation: LeaveRoomState.Confirmation = LeaveRoomState.Confirmation.Hidden, + progress: LeaveRoomState.Progress = LeaveRoomState.Progress.Hidden, + error: LeaveRoomState.Error = LeaveRoomState.Error.Hidden, +) = LeaveRoomState( + confirmation = confirmation, + progress = progress, + error = error, + eventSink = {}, +) diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt index fff7c00410..a508b4f448 100644 --- a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt +++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt @@ -44,7 +44,7 @@ class LeaveRoomPresenterImplTest { @Test fun `present - initial state hides all dialogs`() = runTest { - val presenter = createPresenter() + val presenter = createLeaveRoomPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -57,7 +57,7 @@ class LeaveRoomPresenterImplTest { @Test fun `present - show generic confirmation`() = runTest { - val presenter = createPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -77,7 +77,7 @@ class LeaveRoomPresenterImplTest { @Test fun `present - show private room confirmation`() = runTest { - val presenter = createPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -97,7 +97,7 @@ class LeaveRoomPresenterImplTest { @Test fun `present - show last user in room confirmation`() = runTest { - val presenter = createPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -118,7 +118,7 @@ class LeaveRoomPresenterImplTest { @Test fun `present - leaving a room leaves the room`() = runTest { val roomMembershipObserver = RoomMembershipObserver() - val presenter = createPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -140,7 +140,7 @@ class LeaveRoomPresenterImplTest { @Test fun `present - show error if leave room fails`() = runTest { - val presenter = createPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -164,7 +164,7 @@ class LeaveRoomPresenterImplTest { @Test fun `present - show progress indicator while leaving a room`() = runTest { - val presenter = createPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -186,7 +186,7 @@ class LeaveRoomPresenterImplTest { @Test fun `present - hide error hides the error`() = runTest { - val presenter = createPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -212,7 +212,7 @@ class LeaveRoomPresenterImplTest { } } -private fun TestScope.createPresenter( +private fun TestScope.createLeaveRoomPresenter( client: MatrixClient = FakeMatrixClient(), roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), ): LeaveRoomPresenter = LeaveRoomPresenterImpl( diff --git a/features/leaveroom/test/build.gradle.kts b/features/leaveroom/test/build.gradle.kts new file mode 100644 index 0000000000..56d07b7883 --- /dev/null +++ b/features/leaveroom/test/build.gradle.kts @@ -0,0 +1,30 @@ +/* + * 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") +} + +android { + namespace = "io.element.android.features.leaveroom.test" +} + +dependencies { + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + api(projects.features.leaveroom.api) +} diff --git a/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFake.kt b/features/leaveroom/test/src/main/kotlin/io/element/android/features/leaveroom/fake/FakeLeaveRoomPresenter.kt similarity index 81% rename from features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFake.kt rename to features/leaveroom/test/src/main/kotlin/io/element/android/features/leaveroom/fake/FakeLeaveRoomPresenter.kt index 28c12b54ba..0aac5a0d74 100644 --- a/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFake.kt +++ b/features/leaveroom/test/src/main/kotlin/io/element/android/features/leaveroom/fake/FakeLeaveRoomPresenter.kt @@ -20,9 +20,8 @@ import androidx.compose.runtime.Composable import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.leaveroom.api.LeaveRoomState -import javax.inject.Inject -class LeaveRoomPresenterFake @Inject constructor() : LeaveRoomPresenter { +class FakeLeaveRoomPresenter : LeaveRoomPresenter { val events = mutableListOf() @@ -30,7 +29,12 @@ class LeaveRoomPresenterFake @Inject constructor() : LeaveRoomPresenter { events += event } - private var state = LeaveRoomState(eventSink = ::handleEvent) + private var state = LeaveRoomState( + confirmation = LeaveRoomState.Confirmation.Hidden, + progress = LeaveRoomState.Progress.Hidden, + error = LeaveRoomState.Error.Hidden, + eventSink = ::handleEvent, + ) set(value) { field = value.copy(eventSink = ::handleEvent) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt index d58361a82f..7f53876df8 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt @@ -17,9 +17,9 @@ package io.element.android.features.location.impl.common.permissions data class PermissionsState( - val permissions: Permissions = Permissions.NoneGranted, - val shouldShowRationale: Boolean = false, - val eventSink: (PermissionsEvents) -> Unit = {}, + val permissions: Permissions, + val shouldShowRationale: Boolean, + val eventSink: (PermissionsEvents) -> Unit, ) { sealed interface Permissions { data object AllGranted : Permissions diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt index 5dae23c998..a41449801b 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt @@ -17,11 +17,11 @@ package io.element.android.features.location.impl.send data class SendLocationState( - val permissionDialog: Dialog = Dialog.None, - val mode: Mode = Mode.PinLocation, - val hasLocationPermission: Boolean = false, - val appName: String = "AppName", - val eventSink: (SendLocationEvents) -> Unit = {}, + val permissionDialog: Dialog, + val mode: Mode, + val hasLocationPermission: Boolean, + val appName: String, + val eventSink: (SendLocationEvents) -> Unit, ) { sealed interface Mode { data object SenderLocation : Mode diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationStateProvider.kt index 15f16f593a..7cc3534f19 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationStateProvider.kt @@ -23,35 +23,44 @@ private const val APP_NAME = "ApplicationName" class SendLocationStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - SendLocationState( + aSendLocationState( permissionDialog = SendLocationState.Dialog.None, mode = SendLocationState.Mode.PinLocation, hasLocationPermission = false, - appName = APP_NAME, ), - SendLocationState( + aSendLocationState( permissionDialog = SendLocationState.Dialog.PermissionDenied, mode = SendLocationState.Mode.PinLocation, hasLocationPermission = false, - appName = APP_NAME, ), - SendLocationState( + aSendLocationState( permissionDialog = SendLocationState.Dialog.PermissionRationale, mode = SendLocationState.Mode.PinLocation, hasLocationPermission = false, - appName = APP_NAME, ), - SendLocationState( + aSendLocationState( permissionDialog = SendLocationState.Dialog.None, mode = SendLocationState.Mode.PinLocation, hasLocationPermission = true, - appName = APP_NAME, ), - SendLocationState( + aSendLocationState( permissionDialog = SendLocationState.Dialog.None, mode = SendLocationState.Mode.SenderLocation, hasLocationPermission = true, - appName = APP_NAME, ), ) } + +private fun aSendLocationState( + permissionDialog: SendLocationState.Dialog, + mode: SendLocationState.Mode, + hasLocationPermission: Boolean, +): SendLocationState { + return SendLocationState( + permissionDialog = permissionDialog, + mode = mode, + hasLocationPermission = hasLocationPermission, + appName = APP_NAME, + eventSink = {} + ) +} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/PermissionsStateFactory.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/PermissionsStateFactory.kt new file mode 100644 index 0000000000..8fe90bea57 --- /dev/null +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/PermissionsStateFactory.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.location.impl + +import io.element.android.features.location.impl.common.permissions.PermissionsState + +fun aPermissionsState( + permissions: PermissionsState.Permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale: Boolean = false, +): PermissionsState { + return PermissionsState( + permissions = permissions, + shouldShowRationale = shouldShowRationale, + eventSink = {}, + ) +} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsPresenterFake.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsPresenterFake.kt index ad653e4df4..dfeb18d4db 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsPresenterFake.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsPresenterFake.kt @@ -26,7 +26,11 @@ class PermissionsPresenterFake : PermissionsPresenter { events += event } - private var state = PermissionsState(eventSink = ::handleEvent) + private var state = PermissionsState( + permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale = false, + eventSink = ::handleEvent + ) set(value) { field = value.copy(eventSink = ::handleEvent) } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt index c0b4cb7d35..1bb99f48ce 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt @@ -22,6 +22,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth import im.vector.app.features.analytics.plan.Composer import io.element.android.features.location.api.Location +import io.element.android.features.location.impl.aPermissionsState import io.element.android.features.location.impl.common.actions.FakeLocationActions import io.element.android.features.location.impl.common.permissions.PermissionsEvents import io.element.android.features.location.impl.common.permissions.PermissionsPresenter @@ -65,7 +66,7 @@ class SendLocationPresenterTest { @Test fun `initial state with permissions granted`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.AllGranted, shouldShowRationale = false, ) @@ -92,7 +93,7 @@ class SendLocationPresenterTest { @Test fun `initial state with permissions partially granted`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.SomeGranted, shouldShowRationale = false, ) @@ -119,7 +120,7 @@ class SendLocationPresenterTest { @Test fun `initial state with permissions denied`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, ) @@ -145,7 +146,7 @@ class SendLocationPresenterTest { @Test fun `initial state with permissions denied once`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = true, ) @@ -171,7 +172,7 @@ class SendLocationPresenterTest { @Test fun `rationale dialog dismiss`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = true, ) @@ -202,7 +203,7 @@ class SendLocationPresenterTest { @Test fun `rationale dialog continue`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = true, ) @@ -230,7 +231,7 @@ class SendLocationPresenterTest { @Test fun `permission denied dialog dismiss`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, ) @@ -261,7 +262,7 @@ class SendLocationPresenterTest { @Test fun `share sender location`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.AllGranted, shouldShowRationale = false, ) @@ -317,7 +318,7 @@ class SendLocationPresenterTest { @Test fun `share pin location`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, ) @@ -373,7 +374,7 @@ class SendLocationPresenterTest { @Test fun `composer context passes through analytics`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, ) @@ -419,7 +420,7 @@ class SendLocationPresenterTest { @Test fun `open settings activity`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, ) diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt index 9eb0c3e1e2..28beb77819 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt @@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth import io.element.android.features.location.api.Location +import io.element.android.features.location.impl.aPermissionsState import io.element.android.features.location.impl.common.actions.FakeLocationActions import io.element.android.features.location.impl.common.permissions.PermissionsEvents import io.element.android.features.location.impl.common.permissions.PermissionsPresenter @@ -55,7 +56,7 @@ class ShowLocationPresenterTest { @Test fun `emits initial state with no location permission`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, ) @@ -75,7 +76,7 @@ class ShowLocationPresenterTest { @Test fun `emits initial state location permission denied once`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = true, ) @@ -94,7 +95,7 @@ class ShowLocationPresenterTest { @Test fun `emits initial state with location permission`() = runTest { - permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.AllGranted)) + permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted)) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -109,7 +110,7 @@ class ShowLocationPresenterTest { @Test fun `emits initial state with partial location permission`() = runTest { - permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.SomeGranted)) + permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted)) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -137,7 +138,7 @@ class ShowLocationPresenterTest { @Test fun `centers on user location`() = runTest { - permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.AllGranted)) + permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted)) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -166,7 +167,7 @@ class ShowLocationPresenterTest { @Test fun `rationale dialog dismiss`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = true, ) @@ -197,7 +198,7 @@ class ShowLocationPresenterTest { @Test fun `rationale dialog continue`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = true, ) @@ -225,7 +226,7 @@ class ShowLocationPresenterTest { @Test fun `permission denied dialog dismiss`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, ) @@ -256,7 +257,7 @@ class ShowLocationPresenterTest { @Test fun `open settings activity`() = runTest { permissionsPresenterFake.givenState( - PermissionsState( + aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, ) @@ -290,7 +291,6 @@ class ShowLocationPresenterTest { } } - companion object { private const val A_DESCRIPTION = "My happy place" } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt index b6aea81951..139893d600 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt @@ -16,8 +16,9 @@ package io.element.android.features.login.impl.accountprovider -data class AccountProvider constructor( - val title: String, +data class AccountProvider( + val url: String, + val title: String = url.removePrefix("https://").removePrefix("http://"), val subtitle: String? = null, val isPublic: Boolean = false, val isMatrixOrg: Boolean = false, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt index 71e1abd591..35fd7246f2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.login.impl.accountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.util.LoginConstants open class AccountProviderProvider : PreviewParameterProvider { override val values: Sequence @@ -31,7 +32,7 @@ open class AccountProviderProvider : PreviewParameterProvider { } fun anAccountProvider() = AccountProvider( - title = "matrix.org", + url = LoginConstants.MATRIX_ORG_URL, subtitle = "Matrix.org is an open network for secure, decentralized communication.", isPublic = true, isMatrixOrg = true, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt index 8813a6a037..726e241253 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt @@ -63,7 +63,7 @@ class ChangeServerPresenter @Inject constructor( changeServerAction: MutableState>, ) = launch { suspend { - authenticationService.setHomeserver(data.title).map { + authenticationService.setHomeserver(data.url).map { authenticationService.getHomeserverDetails().value!! // Valid, remember user choice accountProviderDataSource.userSelection(data) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt index 6ef5be06ab..a151b34303 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import io.element.android.features.login.impl.R import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -39,3 +41,12 @@ internal fun SlidingSyncNotSupportedDialog( content = stringResource(R.string.screen_change_server_error_no_sliding_sync_message), ) } + +@PreviewsDayNight +@Composable +internal fun SlidingSyncNotSupportedDialogPreview() = ElementPreview { + SlidingSyncNotSupportedDialog( + onLearnMoreClicked = {}, + onDismiss = {}, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt index dfdb7dcf99..786d8aaeae 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -19,6 +19,7 @@ package io.element.android.features.login.impl.screens.changeaccountprovider import androidx.compose.runtime.Composable import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.changeserver.ChangeServerPresenter +import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Presenter import javax.inject.Inject @@ -33,7 +34,7 @@ class ChangeAccountProviderPresenter @Inject constructor( // Just matrix.org by default for now accountProviders = listOf( AccountProvider( - title = "matrix.org", + url = LoginConstants.MATRIX_ORG_URL, subtitle = null, isPublic = true, isMatrixOrg = true, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt index c950fdf05b..0d375fb2d6 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt @@ -109,6 +109,7 @@ fun ChangeAccountProviderView( // Other AccountProviderView( item = AccountProvider( + url = "", title = stringResource(id = R.string.screen_change_account_provider_other), ), onClick = onOtherProviderClicked diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index a647441800..21896e4408 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -76,7 +76,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( fun handleEvents(event: ConfirmAccountProviderEvents) { when (event) { ConfirmAccountProviderEvents.Continue -> { - localCoroutineScope.submit(accountProvider.title, loginFlowAction) + localCoroutineScope.submit(accountProvider.url, loginFlowAction) } ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt index c2c98101a5..2fcb57af94 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt @@ -27,7 +27,7 @@ data class ConfirmAccountProviderState( val loginFlow: Async, val eventSink: (ConfirmAccountProviderEvents) -> Unit ) { - val submitEnabled: Boolean get() = accountProvider.title.isNotEmpty() && (loginFlow is Async.Uninitialized || loginFlow is Async.Loading) + val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginFlow is Async.Uninitialized || loginFlow is Async.Loading) } sealed interface LoginFlow { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt index 04a4ccbb0e..22862c4467 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt @@ -43,6 +43,7 @@ import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun ConfirmAccountProviderView( @@ -86,7 +87,7 @@ fun ConfirmAccountProviderView( footer = { ButtonColumnMolecule { Button( - text = stringResource(id = R.string.screen_account_provider_continue), + text = stringResource(id = CommonStrings.action_continue), showProgress = isLoading, onClick = { eventSink.invoke(ConfirmAccountProviderEvents.Continue) }, enabled = state.submitEnabled || isLoading, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt index 1f25acc149..8a69cdf442 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt @@ -139,7 +139,7 @@ fun LoginPasswordView( Spacer(modifier = Modifier.weight(1f)) // Submit Button( - text = stringResource(R.string.screen_login_submit), + text = stringResource(CommonStrings.action_continue), showProgress = isLoading, onClick = ::submit, enabled = state.submitEnabled || isLoading, @@ -167,7 +167,7 @@ fun LoginPasswordView( @OptIn(ExperimentalComposeUiApi::class) @Composable -internal fun LoginForm( +private fun LoginForm( state: LoginPasswordState, isLoading: Boolean, onSubmit: () -> Unit, @@ -199,7 +199,7 @@ internal fun LoginForm( eventSink(LoginPasswordEvents.SetLogin(it)) }), placeholder = { - Text(text = stringResource(R.string.screen_login_username_hint)) + Text(text = stringResource(CommonStrings.common_username)) }, onValueChange = { loginFieldState = it @@ -246,7 +246,7 @@ internal fun LoginForm( eventSink(LoginPasswordEvents.SetPassword(it)) }, placeholder = { - Text(text = stringResource(R.string.screen_login_password_hint)) + Text(text = stringResource(CommonStrings.common_password)) }, visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon = { @@ -272,7 +272,7 @@ internal fun LoginForm( } @Composable -internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) { +private fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) { ErrorDialog( title = stringResource(id = CommonStrings.dialog_title_error), content = stringResource(loginError(error)), diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt index b6ffac8bd1..8dce1bd78e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt @@ -19,6 +19,7 @@ package io.element.android.features.login.impl.screens.searchaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.resolver.HomeserverData +import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Async open class SearchAccountProviderStateProvider : PreviewParameterProvider { @@ -49,7 +50,7 @@ fun aHomeserverDataList(): List { } fun aHomeserverData( - homeserverUrl: String = "https://matrix.org", + homeserverUrl: String = LoginConstants.MATRIX_ORG_URL, isWellknownValid: Boolean = true, supportSlidingSync: Boolean = true, ): HomeserverData { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt index 024bc20857..29781acff1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt @@ -54,12 +54,13 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderVie import io.element.android.features.login.impl.changeserver.ChangeServerEvents import io.element.android.features.login.impl.changeserver.ChangeServerView import io.element.android.features.login.impl.resolver.HomeserverData +import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.form.textFieldState -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton @@ -195,9 +196,9 @@ fun SearchAccountProviderView( @Composable private fun HomeserverData.toAccountProvider(): AccountProvider { - val isMatrixOrg = homeserverUrl == "https://matrix.org" + val isMatrixOrg = homeserverUrl == LoginConstants.MATRIX_ORG_URL return AccountProvider( - title = homeserverUrl.removePrefix("http://").removePrefix("https://"), + url = homeserverUrl, subtitle = if (isMatrixOrg) stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle) else null, isPublic = isMatrixOrg, // There is no need to know for other servers right now isMatrixOrg = isMatrixOrg, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt index 152b1a094c..98fd62d7b0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt @@ -19,14 +19,14 @@ package io.element.android.features.login.impl.util import io.element.android.features.login.impl.accountprovider.AccountProvider object LoginConstants { - const val MATRIX_ORG_URL = "matrix.org" + const val MATRIX_ORG_URL = "https://matrix.org" - const val DEFAULT_HOMESERVER_URL = "matrix.org" + const val DEFAULT_HOMESERVER_URL = "https://matrix.org" const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md" } val defaultAccountProvider = AccountProvider( - title = LoginConstants.DEFAULT_HOMESERVER_URL, + url = LoginConstants.DEFAULT_HOMESERVER_URL, subtitle = null, isPublic = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL, isMatrixOrg = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL, diff --git a/features/login/impl/src/main/res/values-zh-rTW/translations.xml b/features/login/impl/src/main/res/values-zh-rTW/translations.xml index e4bfd61b35..789de2b30e 100644 --- a/features/login/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/login/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,13 +1,28 @@ + "更改帳號提供者" + "家伺服器位址" + "輸入關鍵字或網域名稱。" + "搜尋公司、社群、私有伺服器" + "尋找帳號提供者" + "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。" "您即將登入 %s" + "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。" "您即將在 %s 建立帳號" + "Matrix.org 由 Matrix.org 基金會營運,是用於安全、去中心化通訊的公共 Matrix 網路上的大型免費伺服器。" "其他" - "此伺服器目前不支援 sliding sync。" + "使用不同的帳戶提供者,例如您自己的伺服器或工作帳號。" + "更改帳號提供者" + "此伺服器目前不支援滑動同步(sliding sync)。" "家伺服器 URL" + "這個帳號已被停用。" + "不正確的使用者名稱或密碼" "輸入您的詳細資料" "歡迎回來!" "登入 %1$s" + "更改帳號提供者" + "Matrix 是一個開放網路,為了安全、去中心化的通訊而生。" + "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。" "您即將登入 %1$s" "您即將在 %1$s 建立帳號" "歡迎使用 %1$s!" @@ -16,5 +31,6 @@ "選擇您的伺服器" "密碼" "繼續" + "Matrix 是一個開放網路,為了安全、去中心化的通訊而生。" "使用者名稱" diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index f94ce4e65d..44c36128e2 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -63,7 +63,7 @@ class ChangeServerPresenterTest { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized) authenticationService.givenHomeserver(A_HOMESERVER) - initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(A_HOMESERVER_URL))) + initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) val loadingState = awaitItem() assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java) val successState = awaitItem() @@ -83,7 +83,7 @@ class ChangeServerPresenterTest { }.test { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized) - initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(A_HOMESERVER_URL))) + initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) val loadingState = awaitItem() assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java) val failureState = awaitItem() diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt index edf198fe35..4ecbe10b4e 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt @@ -50,6 +50,7 @@ class ChangeAccountProviderPresenterTest { assertThat(initialState.accountProviders).isEqualTo( listOf( AccountProvider( + url = "https://matrix.org", title = "matrix.org", subtitle = null, isPublic = true, diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceScreen.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceView.kt similarity index 98% rename from features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceScreen.kt rename to features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceView.kt index e30a4944a4..24b7c8a266 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceScreen.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceView.kt @@ -74,7 +74,7 @@ fun LogoutPreferenceView( } @Composable -fun LogoutPreferenceContent( +private fun LogoutPreferenceContent( onClick: () -> Unit = {}, ) { PreferenceText( diff --git a/features/logout/impl/build.gradle.kts b/features/logout/impl/build.gradle.kts index 52a9f76b41..f5ee8dd951 100644 --- a/features/logout/impl/build.gradle.kts +++ b/features/logout/impl/build.gradle.kts @@ -38,7 +38,6 @@ dependencies { implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) implementation(projects.libraries.dateformatter.api) - implementation(libs.accompanist.placeholder) api(projects.features.logout.api) ksp(libs.showkase.processor) diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 058dafaf1f..e886a3aeaa 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -54,13 +54,11 @@ dependencies { implementation(projects.services.analytics.api) implementation(libs.coil.compose) implementation(libs.datetime) - implementation(libs.accompanist.flowlayout) implementation(libs.jsoup) implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout.compose) implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.ui) - implementation(libs.accompanist.systemui) implementation(libs.vanniktech.blurhash) implementation(libs.telephoto.zoomableimage) implementation(libs.matrix.emojibase.bindings) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt new file mode 100644 index 0000000000..8b28a216b0 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt @@ -0,0 +1,177 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package io.element.android.features.messages.impl + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.material3.rememberStandardBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.min +import kotlin.math.roundToInt + +/** + * A [BottomSheetScaffold] that allows the sheet to be expanded the screen height + * of the sheet contents. + * + * @param content The main content. + * @param sheetContent The sheet content. + * @param sheetDragHandle The drag handle for the sheet. + * @param sheetSwipeEnabled Whether the sheet can be swiped. This value is ignored and swipe is disabled if the sheet content overflows. + * @param sheetShape The shape of the sheet. + * @param sheetTonalElevation The tonal elevation of the sheet. + * @param sheetShadowElevation The shadow elevation of the sheet. + * @param modifier The modifier for the layout. + * @param sheetContentKey The key for the sheet content. If the key changes, the sheet will be remeasured. + */ +@Composable +internal fun ExpandableBottomSheetScaffold( + content: @Composable (padding: PaddingValues) -> Unit, + sheetContent: @Composable (subcomposing: Boolean) -> Unit, + sheetDragHandle: @Composable () -> Unit, + sheetSwipeEnabled: Boolean, + sheetShape: Shape, + sheetTonalElevation: Dp, + sheetShadowElevation: Dp, + modifier: Modifier = Modifier, + sheetContentKey: Int? = null, +) { + val scaffoldState = rememberBottomSheetScaffoldState( + bottomSheetState = rememberStandardBottomSheetState( + initialValue = SheetValue.PartiallyExpanded, + skipHiddenState = true, + ) + ) + + // If the content overflows, we disable swipe to prevent the sheet from intercepting + // scroll events of the sheet content. + var contentOverflows by remember { mutableStateOf(false) } + val sheetSwipeEnabledIfPossible by remember(contentOverflows, sheetSwipeEnabled) { + derivedStateOf { + sheetSwipeEnabled && !contentOverflows + } + } + + LaunchedEffect(sheetSwipeEnabledIfPossible) { + if (!sheetSwipeEnabledIfPossible) { + scaffoldState.bottomSheetState.partialExpand() + } + } + + @Composable + fun Scaffold( + sheetContent: @Composable () -> Unit, + dragHandle: @Composable () -> Unit, + peekHeight: Dp, + ) { + BottomSheetScaffold( + modifier = Modifier, + scaffoldState = scaffoldState, + sheetPeekHeight = peekHeight, + sheetSwipeEnabled = sheetSwipeEnabledIfPossible, + sheetDragHandle = dragHandle, + sheetShape = sheetShape, + content = content, + sheetContent = { sheetContent() }, + sheetTonalElevation = sheetTonalElevation, + sheetShadowElevation = sheetShadowElevation, + ) + } + + SubcomposeLayout( + modifier = modifier, + measurePolicy = { constraints: Constraints -> + val sheetContentSub = subcompose(Slot.SheetContent(sheetContentKey)) { sheetContent(true) }.map { + it.measure(Constraints(maxWidth = constraints.maxWidth)) + }.first() + val dragHandleSub = subcompose(Slot.DragHandle) { sheetDragHandle() }.map { + it.measure(Constraints(maxWidth = constraints.maxWidth)) + }.firstOrNull() + val dragHandleHeight = dragHandleSub?.height?.toDp() ?: 0.dp + + val maxHeight = constraints.maxHeight.toDp() + val contentHeight = sheetContentSub.height.toDp() + dragHandleHeight + + contentOverflows = contentHeight > maxHeight + + val peekHeight = min( + maxHeight, // prevent the sheet from expanding beyond the screen + contentHeight + ) + + val scaffoldPlaceables = subcompose(Slot.Scaffold) { + Scaffold({ + Layout( + modifier = Modifier.fillMaxHeight(), + measurePolicy = { measurables, constraints -> + val constraintHeight = constraints.maxHeight + val offset = scaffoldState.bottomSheetState.getOffset() ?: 0 + val height = Integer.max(0, constraintHeight - offset) + val top = measurables[0].measure( + constraints.copy( + minHeight = height, + maxHeight = height + ) + ) + layout(constraints.maxWidth, constraints.maxHeight) { + top.place(x = 0, y = 0) + } + }, + content = { sheetContent(false) }) + }, sheetDragHandle, peekHeight) + }.map { measurable: Measurable -> + measurable.measure(constraints) + } + val scaffoldPlaceable = scaffoldPlaceables.first() + layout(constraints.maxWidth, constraints.maxHeight) { + scaffoldPlaceable.place(0, 0) + } + }) +} + +private fun SheetState.getOffset(): Int? = try { + requireOffset().roundToInt() +} catch (e: IllegalStateException) { + null +} + +private sealed class Slot { + data class SheetContent(val key: Int?) : Slot() + data object DragHandle : Slot() + data object Scaffold : Slot() +} + diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index c99c0372a8..20dc17eabc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -64,9 +64,9 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher -import io.element.android.libraries.designsystem.utils.SnackbarMessage -import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage +import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 46aad1e191..31a66acd8c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -25,7 +25,7 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId @Immutable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index a88ebcbcd8..5642bd4d2b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -45,6 +45,7 @@ open class MessagesStateProvider : PreviewParameterProvider { roomName = Async.Uninitialized, roomAvatar = Async.Uninitialized, ), + aMessagesState().copy(composerState = aMessageComposerState().copy(showTextFormatting = true)), ) } @@ -55,9 +56,7 @@ fun aMessagesState() = MessagesState( userHasPermissionToSendMessage = true, userHasPermissionToRedact = false, composerState = aMessageComposerState().copy( - richTextEditorState = RichTextEditorState("Hello", fake = true).apply { - requestFocus() - }, + richTextEditorState = RichTextEditorState("Hello", initialFocus = true), isFullScreen = false, mode = MessageComposerMode.Normal("Hello"), ), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 8cb5bff907..e112a451ad 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -19,8 +19,8 @@ package io.element.android.features.messages.impl import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -32,13 +32,13 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle @@ -50,6 +50,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.messagecomposer.AttachmentsBottomSheet import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerView @@ -71,14 +72,15 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.LogCompositions -import io.element.android.libraries.designsystem.utils.SnackbarHost -import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost +import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.theme.ElementTheme @@ -86,7 +88,6 @@ import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import timber.log.Timber -@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable fun MessagesView( state: MessagesState, @@ -277,40 +278,58 @@ private fun MessagesViewContent( modifier: Modifier = Modifier, onSwipeToReply: (TimelineItem.Event) -> Unit, ) { - Column( + Box( modifier = modifier .fillMaxSize() .navigationBarsPadding() - .imePadding() + .imePadding(), ) { - // Hide timeline if composer is full screen - if (!state.composerState.isFullScreen) { - TimelineView( - state = state.timelineState, - modifier = Modifier.weight(1f), - onMessageClicked = onMessageClicked, - onMessageLongClicked = onMessageLongClicked, - onUserDataClicked = onUserDataClicked, - onTimestampClicked = onTimestampClicked, - onReactionClicked = onReactionClicked, - onReactionLongClicked = onReactionLongClicked, - onMoreReactionsClicked = onMoreReactionsClicked, - onSwipeToReply = onSwipeToReply, - ) - } - if (state.userHasPermissionToSendMessage) { - MessageComposerView( - state = state.composerState, - onSendLocationClicked = onSendLocationClicked, - onCreatePollClicked = onCreatePollClicked, - enableTextFormatting = state.enableTextFormatting, - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(Alignment.Bottom) - ) - } else { - CantSendMessageBanner() - } + AttachmentsBottomSheet( + state = state.composerState, + onSendLocationClicked = onSendLocationClicked, + onCreatePollClicked = onCreatePollClicked, + enableTextFormatting = state.enableTextFormatting, + ) + + ExpandableBottomSheetScaffold( + sheetDragHandle = if (state.composerState.showTextFormatting) { + @Composable { BottomSheetDragHandle() } + } else { + @Composable {} + }, + sheetSwipeEnabled = state.composerState.showTextFormatting, + sheetShape = if (state.composerState.showTextFormatting) MaterialTheme.shapes.large else RectangleShape, + content = { paddingValues -> + TimelineView( + modifier = Modifier.padding(paddingValues), + state = state.timelineState, + onMessageClicked = onMessageClicked, + onMessageLongClicked = onMessageLongClicked, + onUserDataClicked = onUserDataClicked, + onTimestampClicked = onTimestampClicked, + onReactionClicked = onReactionClicked, + onReactionLongClicked = onReactionLongClicked, + onMoreReactionsClicked = onMoreReactionsClicked, + onSwipeToReply = onSwipeToReply, + ) + }, + sheetContent = { subcomposing: Boolean -> + if (state.userHasPermissionToSendMessage) { + MessageComposerView( + state = state.composerState, + subcomposing = subcomposing, + enableTextFormatting = state.enableTextFormatting, + modifier = Modifier + .fillMaxWidth(), + ) + } else { + CantSendMessageBanner() + } + }, + sheetContentKey = state.composerState.richTextEditorState.lineCount, + sheetTonalElevation = 0.dp, + sheetShadowElevation = 0.dp, + ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index 1435573fbe..05db97d3be 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -336,7 +336,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif private val emojiRippleRadius = 24.dp @Composable -internal fun EmojiReactionsRow( +private fun EmojiReactionsRow( highlightedEmojis: ImmutableList, onEmojiReactionClicked: (String) -> Unit, onCustomReactionClicked: () -> Unit, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt index 56d7f63eb1..dbcb4c8c3c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt @@ -20,8 +20,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails import io.element.android.libraries.matrix.api.room.message.RoomMessage +import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -29,29 +29,37 @@ open class ForwardMessagesStateProvider : PreviewParameterProvider get() = sequenceOf( aForwardMessagesState(), - aForwardMessagesState(query = "Test"), + aForwardMessagesState(query = "Test", isSearchActive = true), aForwardMessagesState(resultState = SearchBarResultState.Results(aForwardMessagesRoomList())), - aForwardMessagesState(resultState = SearchBarResultState.Results(aForwardMessagesRoomList()), query = "Test"), aForwardMessagesState( resultState = SearchBarResultState.Results(aForwardMessagesRoomList()), query = "Test", + isSearchActive = true, + ), + aForwardMessagesState( + resultState = SearchBarResultState.Results(aForwardMessagesRoomList()), + query = "Test", + isSearchActive = true, selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))) ), aForwardMessagesState( resultState = SearchBarResultState.Results(aForwardMessagesRoomList()), query = "Test", + isSearchActive = true, selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))), isForwarding = true, ), aForwardMessagesState( resultState = SearchBarResultState.Results(aForwardMessagesRoomList()), query = "Test", + isSearchActive = true, selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))), forwardingSucceeded = persistentListOf(RoomId("!room2:domain")), ), aForwardMessagesState( resultState = SearchBarResultState.Results(aForwardMessagesRoomList()), query = "Test", + isSearchActive = true, selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))), error = Throwable("error"), ), @@ -78,7 +86,7 @@ fun aForwardMessagesState( eventSink = {} ) -internal fun aForwardMessagesRoomList() = listOf( +internal fun aForwardMessagesRoomList() = persistentListOf( aRoomDetailsState(), aRoomDetailsState(roomId = RoomId("!room2:domain"), canonicalAlias = "#element-x-room:matrix.org"), ) @@ -94,13 +102,13 @@ fun aRoomDetailsState( unreadNotificationCount: Int = 0, inviter: RoomMember? = null, ) = RoomSummaryDetails( - roomId = roomId, - name = name, - canonicalAlias = canonicalAlias, - isDirect = isDirect, - avatarURLString = avatarURLString, - lastMessage = lastMessage, - lastMessageTimestamp = lastMessageTimestamp, - unreadNotificationCount = unreadNotificationCount, - inviter = inviter, - ) + roomId = roomId, + name = name, + canonicalAlias = canonicalAlias, + isDirect = isDirect, + avatarURLString = avatarURLString, + lastMessage = lastMessage, + lastMessageTimestamp = lastMessageTimestamp, + unreadNotificationCount = unreadNotificationCount, + inviter = inviter, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt index f21a7cd335..1174c0ffcc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt @@ -135,7 +135,7 @@ fun ForwardMessagesView( .padding(paddingValues) .consumeWindowInsets(paddingValues) ) { - SearchBar>( + SearchBar( placeHolderTitle = stringResource(CommonStrings.action_search), query = state.query, onQueryChange = { state.eventSink(ForwardMessagesEvents.UpdateQuery(it)) }, @@ -204,7 +204,7 @@ fun ForwardMessagesView( } @Composable -internal fun SelectedRooms( +private fun SelectedRooms( selectedRooms: ImmutableList, onRoomRemoved: (RoomSummaryDetails) -> Unit, modifier: Modifier = Modifier, @@ -221,7 +221,7 @@ internal fun SelectedRooms( } @Composable -internal fun RoomSummaryView( +private fun RoomSummaryView( summary: RoomSummaryDetails, isSelected: Boolean, onSelection: (RoomSummaryDetails) -> Unit, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt index 631ff71f91..3ca534c310 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -142,7 +142,7 @@ private fun MediaImageView( @UnstableApi @Composable -fun MediaVideoView( +private fun MediaVideoView( localMediaViewState: LocalMediaViewState, localMedia: LocalMedia?, modifier: Modifier = Modifier, @@ -196,7 +196,7 @@ fun MediaVideoView( } @Composable -fun MediaPDFView( +private fun MediaPDFView( localMediaViewState: LocalMediaViewState, localMedia: LocalMedia?, zoomableState: ZoomableState, @@ -211,7 +211,7 @@ fun MediaPDFView( } @Composable -fun MediaFileView( +private fun MediaFileView( localMediaViewState: LocalMediaViewState, uri: Uri?, info: MediaInfo?, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt index b295f68e11..91fff1fa72 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt @@ -34,9 +34,9 @@ import io.element.android.features.messages.impl.media.local.LocalMediaActions import io.element.android.features.messages.impl.media.local.LocalMediaFactory import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher -import io.element.android.libraries.designsystem.utils.SnackbarMessage -import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage +import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.ui.strings.CommonStrings diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt index 18375746c5..86abaf648e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt @@ -19,7 +19,7 @@ package io.element.android.features.messages.impl.media.viewer import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.features.messages.impl.media.local.MediaInfo import io.element.android.libraries.architecture.Async -import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.media.MediaSource data class MediaViewerState( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index b0570a6f2a..cb2775c7b6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -29,7 +29,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.OpenInNew +import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LinearProgressIndicator import androidx.compose.runtime.Composable @@ -62,8 +62,8 @@ import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.designsystem.utils.SnackbarHost -import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost +import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.ui.strings.CommonStrings @@ -185,7 +185,7 @@ private fun MediaViewerTopBar( contentDescription = stringResource(id = CommonStrings.common_install_apk_android) ) else -> Icon( - imageVector = Icons.Default.OpenInNew, + imageVector = Icons.AutoMirrored.Filled.OpenInNew, contentDescription = stringResource(id = CommonStrings.action_open_with) ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt index f67a3a54e1..b920e5967d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt @@ -95,7 +95,7 @@ internal fun AttachmentsBottomSheet( @OptIn(ExperimentalMaterialApi::class) @Composable -internal fun AttachmentSourcePickerMenu( +private fun AttachmentSourcePickerMenu( state: MessageComposerState, onSendLocationClicked: () -> Unit, onCreatePollClicked: () -> Unit, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index d6e74a0df0..a45056ac67 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -35,8 +35,8 @@ import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.media.local.LocalMediaFactory import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher -import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -155,7 +155,9 @@ class MessageComposerPresenter @Inject constructor( when (event) { MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value MessageComposerEvents.CloseSpecialMode -> { - richTextEditorState.setHtml("") + localCoroutineScope.launch { + richTextEditorState.setHtml("") + } messageComposerContext.composerMode = MessageComposerMode.Normal("") } is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage( @@ -165,9 +167,14 @@ class MessageComposerPresenter @Inject constructor( ) is MessageComposerEvents.SetMode -> { messageComposerContext.composerMode = event.composerMode - if (event.composerMode is MessageComposerMode.Reply) { + when (event.composerMode) { + is MessageComposerMode.Reply -> event.composerMode.eventId + is MessageComposerMode.Edit -> event.composerMode.eventId + is MessageComposerMode.Normal -> null + is MessageComposerMode.Quote -> null + }.let { relatedEventId -> appCoroutineScope.launch { - room.enterReplyMode(event.composerMode.eventId) + room.enterSpecialMode(relatedEventId) } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index 65fac53fdc..ff74a81abe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -17,12 +17,13 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.ImmutableList -@Immutable +@Stable data class MessageComposerState( val richTextEditorState: RichTextEditorState, val isFullScreen: Boolean, @@ -34,7 +35,6 @@ data class MessageComposerState( val attachmentsState: AttachmentsState, val eventSink: (MessageComposerEvents) -> Unit, ) { - val canSendMessage: Boolean = richTextEditorState.messageHtml.isNotEmpty() val hasFocus: Boolean = richTextEditorState.hasFocus } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index d86969fc19..0483d945af 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -28,8 +28,7 @@ open class MessageComposerStateProvider : PreviewParameterProvider Unit, - onCreatePollClicked: () -> Unit, + subcomposing: Boolean, enableTextFormatting: Boolean, modifier: Modifier = Modifier, ) { - fun onFullscreenToggle() { - state.eventSink(MessageComposerEvents.ToggleFullScreenState) - } - fun sendMessage(message: Message) { state.eventSink(MessageComposerEvents.SendMessage(message)) } @@ -57,37 +57,44 @@ fun MessageComposerView( state.eventSink(MessageComposerEvents.Error(error)) } - Box(modifier = modifier) { - AttachmentsBottomSheet( - state = state, - onSendLocationClicked = onSendLocationClicked, - onCreatePollClicked = onCreatePollClicked, - enableTextFormatting = enableTextFormatting, - ) - - TextComposer( - state = state.richTextEditorState, - canSendMessage = state.canSendMessage, - onRequestFocus = { state.richTextEditorState.requestFocus() }, - onSendMessage = ::sendMessage, - composerMode = state.mode, - showTextFormatting = state.showTextFormatting, - onResetComposerMode = ::onCloseSpecialMode, - onAddAttachment = ::onAddAttachment, - onDismissTextFormatting = ::onDismissTextFormatting, - enableTextFormatting = enableTextFormatting, - onError = ::onError, - ) + val coroutineScope = rememberCoroutineScope() + fun onRequestFocus() { + coroutineScope.launch { + state.richTextEditorState.requestFocus() + } } + + TextComposer( + modifier = modifier, + state = state.richTextEditorState, + subcomposing = subcomposing, + onRequestFocus = ::onRequestFocus, + onSendMessage = ::sendMessage, + composerMode = state.mode, + showTextFormatting = state.showTextFormatting, + onResetComposerMode = ::onCloseSpecialMode, + onAddAttachment = ::onAddAttachment, + onDismissTextFormatting = ::onDismissTextFormatting, + enableTextFormatting = enableTextFormatting, + onError = ::onError, + ) } @PreviewsDayNight @Composable internal fun MessageComposerViewPreview(@PreviewParameter(MessageComposerStateProvider::class) state: MessageComposerState) = ElementPreview { - MessageComposerView( - state = state, - onSendLocationClicked = {}, - onCreatePollClicked = {}, - enableTextFormatting = true, - ) + Column { + MessageComposerView( + modifier = Modifier.height(IntrinsicSize.Min), + state = state, + enableTextFormatting = true, + subcomposing = false, + ) + MessageComposerView( + modifier = Modifier.height(200.dp), + state = state, + enableTextFormatting = true, + subcomposing = false, + ) + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt index 0ce4856ffa..d474e3d25f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt @@ -30,8 +30,8 @@ import dagger.assisted.AssistedInject import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher -import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index ba02b8eb95..fdb4b309eb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -155,7 +155,7 @@ fun TimelineView( } @Composable -fun TimelineItemRow( +private fun TimelineItemRow( timelineItem: TimelineItem, highlightedItem: String?, userHasPermissionToSendMessage: Boolean, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 4ab98ce953..0ef1c9bc67 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -213,7 +213,7 @@ fun TimelineItemEventRow( * @param content the content to display. */ @Composable -fun SwipeSensitivity( +private fun SwipeSensitivity( sensitivityFactor: Float, content: @Composable () -> Unit, ) { @@ -306,7 +306,7 @@ private fun TimelineItemEventRowContent( // Reactions if (event.reactionsState.reactions.isNotEmpty()) { - TimelineItemReactions( + TimelineItemReactionsView( reactionsState = event.reactionsState, isOutgoing = event.isMine, onReactionClicked = onReactionClicked, @@ -315,7 +315,7 @@ private fun TimelineItemEventRowContent( modifier = Modifier .constrainAs(reactions) { top.linkTo(message.bottom, margin = (-4).dp) - this.linkStartOrEnd(event) + linkStartOrEnd(event) } .zIndex(1f) .padding(start = if (event.isMine) 16.dp else 36.dp, end = 16.dp) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt index ac1889a684..3f625fd6e4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt @@ -37,7 +37,7 @@ import io.element.android.libraries.designsystem.utils.CommonDrawables import kotlinx.collections.immutable.ImmutableList @Composable -fun TimelineItemReactions( +fun TimelineItemReactionsView( reactionsState: TimelineItemReactions, isOutgoing: Boolean, onReactionClicked: (emoji: String) -> Unit, @@ -46,16 +46,16 @@ fun TimelineItemReactions( modifier: Modifier = Modifier, ) { var expanded: Boolean by rememberSaveable { mutableStateOf(false) } - TimelineItemReactionsView( - modifier = modifier, - reactions = reactionsState.reactions, - expanded = expanded, - isOutgoing = isOutgoing, - onReactionClick = onReactionClicked, - onReactionLongClick = onReactionLongClicked, - onMoreReactionsClick = onMoreReactionsClicked, - onToggleExpandClick = { expanded = !expanded }, - ) + TimelineItemReactionsView( + modifier = modifier, + reactions = reactionsState.reactions, + expanded = expanded, + isOutgoing = isOutgoing, + onReactionClick = onReactionClicked, + onReactionLongClick = onReactionLongClicked, + onMoreReactionsClick = onMoreReactionsClicked, + onToggleExpandClick = { expanded = !expanded }, + ) } @Composable @@ -153,7 +153,7 @@ private fun ContentToPreview( reactions: ImmutableList, isOutgoing: Boolean = false ) { - TimelineItemReactions( + TimelineItemReactionsView( reactionsState = TimelineItemReactions( reactions ), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt index 2458686a83..c9936855f1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt @@ -20,5 +20,5 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem sealed interface CustomReactionEvents { data class ShowCustomReactionSheet(val event: TimelineItem.Event) : CustomReactionEvents - object DismissCustomReactionSheet : CustomReactionEvents + data object DismissCustomReactionSheet : CustomReactionEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt index dc0e8a190f..ee9d4c819d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt @@ -34,8 +34,8 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.SecondaryTabRow import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -49,8 +49,8 @@ import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.EmojibaseCategory import io.element.android.emojibasebindings.EmojibaseDatasource import io.element.android.emojibasebindings.EmojibaseStore -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme @@ -68,12 +68,12 @@ fun EmojiPicker( ) { val coroutineScope = rememberCoroutineScope() val categories = remember { emojibaseStore.categories } - val pagerState = rememberPagerState(pageCount = { EmojibaseCategory.values().size }) + val pagerState = rememberPagerState(pageCount = { EmojibaseCategory.entries.size }) Column(modifier) { - TabRow( + SecondaryTabRow( selectedTabIndex = pagerState.currentPage, ) { - EmojibaseCategory.values().forEachIndexed { index, category -> + EmojibaseCategory.entries.forEachIndexed { index, category -> Tab( text = { Icon( @@ -93,7 +93,7 @@ fun EmojiPicker( state = pagerState, modifier = Modifier.fillMaxWidth(), ) { index -> - val category = EmojibaseCategory.values()[index] + val category = EmojibaseCategory.entries[index] val emojis = categories[category] ?: listOf() LazyVerticalGrid( modifier = Modifier.fillMaxSize(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt similarity index 100% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt index 344f7d6a9e..b9260e2768 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt @@ -160,7 +160,7 @@ private fun SheetContent( } @Composable -fun AggregatedReactionButton( +private fun AggregatedReactionButton( reaction: AggregatedReaction, isHighlighted: Boolean, onClick: () -> Unit, @@ -215,7 +215,7 @@ fun AggregatedReactionButton( } @Composable -fun SenderRow( +private fun SenderRow( avatarData: AvatarData, name: String, userId: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt index 748b163aa3..6959c448cb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch @Composable @@ -71,7 +72,7 @@ internal fun RetrySendMessageMenu( @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun RetrySendMessageMenuBottomSheet( +private fun RetrySendMessageMenuBottomSheet( isVisible: Boolean, onRetry: () -> Unit, onRemoveFailed: () -> Unit, @@ -133,7 +134,7 @@ private fun ColumnScope.RetrySendMenuContents( ListItem( headlineContent = { Text( - text = stringResource(R.string.screen_room_retry_send_menu_remove_action), + text = stringResource(CommonStrings.action_remove), style = ElementTheme.typography.fontBodyLgRegular, ) }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemLoadingMoreIndicator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineLoadingMoreIndicator.kt similarity index 100% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemLoadingMoreIndicator.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineLoadingMoreIndicator.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt index 6a74fd11a5..55e889f496 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParse import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent @@ -49,7 +50,10 @@ class TimelineItemContentFactory @Inject constructor( return when (val itemContent = eventTimelineItem.content) { is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent) is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent) - is MessageContent -> messageFactory.create(itemContent) + is MessageContent -> { + val senderDisplayName = (eventTimelineItem.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: eventTimelineItem.sender.value + messageFactory.create(itemContent, senderDisplayName) + } is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem) is RedactedContent -> redactedMessageFactory.create(itemContent) is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index 3d0dc4d80c..ae2ea4f350 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -25,7 +25,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor import io.element.android.features.messages.impl.timeline.util.toHtmlDocument @@ -39,6 +38,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessa import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import javax.inject.Inject @@ -47,11 +47,11 @@ class TimelineItemContentMessageFactory @Inject constructor( private val fileExtensionExtractor: FileExtensionExtractor, ) { - fun create(content: MessageContent): TimelineItemEventContent { + fun create(content: MessageContent, senderDisplayName: String): TimelineItemEventContent { return when (val messageType = content.type) { is EmoteMessageType -> TimelineItemEmoteContent( - body = messageType.body, - htmlDocument = messageType.formatted?.toHtmlDocument(), + body = "* $senderDisplayName ${messageType.body}", + htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* senderDisplayName"), isEdited = content.isEdited, ) is ImageMessageType -> { @@ -130,7 +130,12 @@ class TimelineItemContentMessageFactory @Inject constructor( htmlDocument = messageType.formatted?.toHtmlDocument(), isEdited = content.isEdited, ) - else -> TimelineItemUnknownContent + UnknownMessageType -> TimelineItemTextContent( + // Display the body as a fallback + body = content.body, + htmlDocument = null, + isEdited = content.isEdited, + ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index 8f00dfb0dd..bd3090e390 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -54,6 +54,7 @@ sealed interface TimelineItem { @Immutable data class Event( val id: String, + // Note: eventId can be null when the event is a local echo val eventId: EventId? = null, val transactionId: TransactionId? = null, val senderId: UserId, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRedactedContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRedactedContent.kt index 7a8edae953..853ab3f030 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRedactedContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRedactedContent.kt @@ -16,6 +16,6 @@ package io.element.android.features.messages.impl.timeline.model.event -object TimelineItemRedactedContent : TimelineItemEventContent{ +data object TimelineItemRedactedContent : TimelineItemEventContent { override val type: String = "TimelineItemRedactedContent" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/toHtmlDocument.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/toHtmlDocument.kt index 8031e77a4d..a38b631eb2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/toHtmlDocument.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/toHtmlDocument.kt @@ -21,8 +21,12 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat import org.jsoup.Jsoup import org.jsoup.nodes.Document -fun FormattedBody.toHtmlDocument(): Document? { +fun FormattedBody.toHtmlDocument(prefix: String? = null): Document? { return takeIf { it.format == MessageFormat.HTML }?.body?.let { formattedBody -> - Jsoup.parse(formattedBody) + if (prefix != null) { + Jsoup.parse("$prefix $formattedBody") + } else { + Jsoup.parse(formattedBody) + } } } diff --git a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml index 1038508ac6..5f3b2b6393 100644 --- a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml @@ -7,6 +7,7 @@ "拍照" "錄影" "附件" + "照片與影片庫" "位置" "投票" "格式化文字" @@ -23,6 +24,7 @@ "更多" "重傳" "無法傳送您的訊息" + "新增表情符號" "較少" "移除" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 9bbb514117..def74c108a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -51,7 +51,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore @@ -96,7 +96,7 @@ class MessagesPresenterTest { @Test fun `present - initial state`() = runTest { - val presenter = createMessagePresenter() + val presenter = createMessagesPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -117,7 +117,7 @@ class MessagesPresenterTest { fun `present - handle toggling a reaction`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) val room = FakeMatrixRoom() - val presenter = createMessagePresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) + val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -137,7 +137,7 @@ class MessagesPresenterTest { fun `present - handle toggling a reaction twice`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) val room = FakeMatrixRoom() - val presenter = createMessagePresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) + val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -154,7 +154,7 @@ class MessagesPresenterTest { @Test fun `present - handle action forward`() = runTest { val navigator = FakeMessagesNavigator() - val presenter = createMessagePresenter(navigator = navigator) + val presenter = createMessagesPresenter(navigator = navigator) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -169,7 +169,7 @@ class MessagesPresenterTest { fun `present - handle action copy`() = runTest { val clipboardHelper = FakeClipboardHelper() val event = aMessageEvent() - val presenter = createMessagePresenter(clipboardHelper = clipboardHelper) + val presenter = createMessagesPresenter(clipboardHelper = clipboardHelper) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -183,7 +183,7 @@ class MessagesPresenterTest { @Test fun `present - handle action reply`() = runTest { - val presenter = createMessagePresenter() + val presenter = createMessagesPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -198,7 +198,7 @@ class MessagesPresenterTest { @Test fun `present - handle action reply to an event with no id does nothing`() = runTest { - val presenter = createMessagePresenter() + val presenter = createMessagesPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -212,7 +212,7 @@ class MessagesPresenterTest { @Test fun `present - handle action reply to an image media message`() = runTest { - val presenter = createMessagePresenter() + val presenter = createMessagesPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -243,7 +243,7 @@ class MessagesPresenterTest { @Test fun `present - handle action reply to a video media message`() = runTest { - val presenter = createMessagePresenter() + val presenter = createMessagesPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -275,7 +275,7 @@ class MessagesPresenterTest { @Test fun `present - handle action reply to a file media message`() = runTest { - val presenter = createMessagePresenter() + val presenter = createMessagesPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -302,7 +302,7 @@ class MessagesPresenterTest { @Test fun `present - handle action edit`() = runTest { - val presenter = createMessagePresenter() + val presenter = createMessagesPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -319,7 +319,7 @@ class MessagesPresenterTest { fun `present - handle action redact`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) val matrixRoom = FakeMatrixRoom() - val presenter = createMessagePresenter(matrixRoom = matrixRoom, coroutineDispatchers = coroutineDispatchers) + val presenter = createMessagesPresenter(matrixRoom = matrixRoom, coroutineDispatchers = coroutineDispatchers) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -335,7 +335,7 @@ class MessagesPresenterTest { @Test fun `present - handle action report content`() = runTest { val navigator = FakeMessagesNavigator() - val presenter = createMessagePresenter(navigator = navigator) + val presenter = createMessagesPresenter(navigator = navigator) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -348,7 +348,7 @@ class MessagesPresenterTest { @Test fun `present - handle dismiss action`() = runTest { - val presenter = createMessagePresenter() + val presenter = createMessagesPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -362,7 +362,7 @@ class MessagesPresenterTest { @Test fun `present - handle action show developer info`() = runTest { val navigator = FakeMessagesNavigator() - val presenter = createMessagePresenter(navigator = navigator) + val presenter = createMessagesPresenter(navigator = navigator) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -376,7 +376,7 @@ class MessagesPresenterTest { @Test fun `present - shows prompt to reinvite users in DM`() = runTest { val room = FakeMatrixRoom(sessionId = A_SESSION_ID, isDirect = true, activeMemberCount = 1L) - val presenter = createMessagePresenter(matrixRoom = room) + val presenter = createMessagesPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -402,7 +402,7 @@ class MessagesPresenterTest { @Test fun `present - doesn't show reinvite prompt in non-direct room`() = runTest { val room = FakeMatrixRoom(sessionId = A_SESSION_ID, isDirect = false, activeMemberCount = 1L) - val presenter = createMessagePresenter(matrixRoom = room) + val presenter = createMessagesPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -418,7 +418,7 @@ class MessagesPresenterTest { @Test fun `present - doesn't show reinvite prompt if other party is present`() = runTest { val room = FakeMatrixRoom(sessionId = A_SESSION_ID, isDirect = true, activeMemberCount = 2L) - val presenter = createMessagePresenter(matrixRoom = room) + val presenter = createMessagesPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -442,7 +442,7 @@ class MessagesPresenterTest { ) ) ) - val presenter = createMessagePresenter(matrixRoom = room) + val presenter = createMessagesPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -469,7 +469,7 @@ class MessagesPresenterTest { ) ) ) - val presenter = createMessagePresenter(matrixRoom = room) + val presenter = createMessagesPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -490,7 +490,7 @@ class MessagesPresenterTest { fun `present - handle reinviting other user when memberlist is not ready`() = runTest { val room = FakeMatrixRoom(sessionId = A_SESSION_ID) room.givenRoomMembersState(MatrixRoomMembersState.Unknown) - val presenter = createMessagePresenter(matrixRoom = room) + val presenter = createMessagesPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -516,7 +516,7 @@ class MessagesPresenterTest { ) ) room.givenInviteUserResult(Result.failure(Throwable("Oops!"))) - val presenter = createMessagePresenter(matrixRoom = room) + val presenter = createMessagesPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -537,7 +537,7 @@ class MessagesPresenterTest { fun `present - permission to post`() = runTest { val matrixRoom = FakeMatrixRoom() matrixRoom.givenCanSendEventResult(MessageEventType.ROOM_MESSAGE, Result.success(true)) - val presenter = createMessagePresenter(matrixRoom = matrixRoom) + val presenter = createMessagesPresenter(matrixRoom = matrixRoom) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -550,7 +550,7 @@ class MessagesPresenterTest { fun `present - no permission to post`() = runTest { val matrixRoom = FakeMatrixRoom() matrixRoom.givenCanSendEventResult(MessageEventType.ROOM_MESSAGE, Result.success(false)) - val presenter = createMessagePresenter(matrixRoom = matrixRoom) + val presenter = createMessagesPresenter(matrixRoom = matrixRoom) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -565,7 +565,7 @@ class MessagesPresenterTest { @Test fun `present - permission to redact`() = runTest { val matrixRoom = FakeMatrixRoom(canRedact = true) - val presenter = createMessagePresenter(matrixRoom = matrixRoom) + val presenter = createMessagesPresenter(matrixRoom = matrixRoom) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -579,7 +579,7 @@ class MessagesPresenterTest { fun `present - handle poll end`() = runTest { val room = FakeMatrixRoom() val analyticsService = FakeAnalyticsService() - val presenter = createMessagePresenter( + val presenter = createMessagesPresenter( matrixRoom = room, analyticsService = analyticsService, ) @@ -598,7 +598,7 @@ class MessagesPresenterTest { } } - private fun TestScope.createMessagePresenter( + private fun TestScope.createMessagesPresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), matrixRoom: MatrixRoom = FakeMatrixRoom(), navigator: FakeMessagesNavigator = FakeMessagesNavigator(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt index 316e39317e..f94e79f0cb 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt @@ -46,7 +46,7 @@ class ActionListPresenterTest { @Test fun `present - initial state`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = true) + val presenter = createActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -57,7 +57,7 @@ class ActionListPresenterTest { @Test fun `present - compute for message from me redacted`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = true) + val presenter = createActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -82,7 +82,7 @@ class ActionListPresenterTest { @Test fun `present - compute for message from others redacted`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = true) + val presenter = createActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -107,7 +107,7 @@ class ActionListPresenterTest { @Test fun `present - compute for others message`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = true) + val presenter = createActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -139,7 +139,7 @@ class ActionListPresenterTest { @Test fun `present - compute for others message cannot sent message`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = true) + val presenter = createActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -170,7 +170,7 @@ class ActionListPresenterTest { @Test fun `present - compute for others message and can redact`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = true) + val presenter = createActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -201,7 +201,7 @@ class ActionListPresenterTest { @Test fun `present - compute for my message`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = true) + val presenter = createActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -234,7 +234,7 @@ class ActionListPresenterTest { @Test fun `present - compute for a media item`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = true) + val presenter = createActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -265,7 +265,7 @@ class ActionListPresenterTest { @Test fun `present - compute for a state item in debug build`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = true) + val presenter = createActionListPresenter(isDeveloperModeEnabled = true) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -294,7 +294,7 @@ class ActionListPresenterTest { @Test fun `present - compute for a state item in non-debuggable build`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = false) + val presenter = createActionListPresenter(isDeveloperModeEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -322,7 +322,7 @@ class ActionListPresenterTest { @Test fun `present - compute message in non-debuggable build`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = false) + val presenter = createActionListPresenter(isDeveloperModeEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -354,7 +354,7 @@ class ActionListPresenterTest { @Test fun `present - compute message with no actions`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = false) + val presenter = createActionListPresenter(isDeveloperModeEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -381,7 +381,7 @@ class ActionListPresenterTest { @Test fun `present - compute not sent message`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = false) + val presenter = createActionListPresenter(isDeveloperModeEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -410,7 +410,7 @@ class ActionListPresenterTest { @Test fun `present - compute for poll message`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = false) + val presenter = createActionListPresenter(isDeveloperModeEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -436,7 +436,7 @@ class ActionListPresenterTest { @Test fun `present - compute for ended poll message`() = runTest { - val presenter = anActionListPresenter(isDeveloperModeEnabled = false) + val presenter = createActionListPresenter(isDeveloperModeEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -460,7 +460,7 @@ class ActionListPresenterTest { } } -private fun anActionListPresenter(isDeveloperModeEnabled: Boolean): ActionListPresenter { +private fun createActionListPresenter(isDeveloperModeEnabled: Boolean): ActionListPresenter { val preferencesStore = InMemoryPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled) return ActionListPresenter(preferencesStore = preferencesStore) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt index 30c8d761b3..22f28f21d0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt @@ -59,7 +59,7 @@ class AttachmentsPreviewPresenterTest { Pair(10, 10) ) ) - val presenter = anAttachmentsPreviewPresenter(room = room) + val presenter = createAttachmentsPreviewPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -81,7 +81,7 @@ class AttachmentsPreviewPresenterTest { val room = FakeMatrixRoom() val failure = MediaPreProcessor.Failure(null) room.givenSendMediaResult(Result.failure(failure)) - val presenter = anAttachmentsPreviewPresenter(room = room) + val presenter = createAttachmentsPreviewPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -101,7 +101,7 @@ class AttachmentsPreviewPresenterTest { @Test fun `present - dismissing the progress dialog stops media upload`() = runTest { - val presenter = anAttachmentsPreviewPresenter() + val presenter = createAttachmentsPreviewPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -114,7 +114,7 @@ class AttachmentsPreviewPresenterTest { } } - private fun anAttachmentsPreviewPresenter( + private fun createAttachmentsPreviewPresenter( localMedia: LocalMedia = aLocalMedia( uri = mockMediaUrl, ), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt index 6401f60f47..79c865ce92 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt @@ -30,7 +30,7 @@ import io.element.android.features.messages.impl.media.viewer.MediaViewerPresent import io.element.android.features.messages.media.FakeLocalMediaActions import io.element.android.features.messages.media.FakeLocalMediaFactory import io.element.android.libraries.architecture.Async -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.matrix.test.media.FakeMediaLoader import io.element.android.libraries.matrix.test.media.aMediaSource import io.element.android.tests.testutils.WarmUpRule @@ -55,7 +55,7 @@ class MediaViewerPresenterTest { fun `present - download media success scenario`() = runTest { val mediaLoader = FakeMediaLoader() val mediaActions = FakeLocalMediaActions() - val presenter = aMediaViewerPresenter(mediaLoader, mediaActions) + val presenter = createMediaViewerPresenter(mediaLoader, mediaActions) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -76,7 +76,7 @@ class MediaViewerPresenterTest { val mediaLoader = FakeMediaLoader() val mediaActions = FakeLocalMediaActions() val snackbarDispatcher = SnackbarDispatcher() - val presenter = aMediaViewerPresenter(mediaLoader, mediaActions, snackbarDispatcher) + val presenter = createMediaViewerPresenter(mediaLoader, mediaActions, snackbarDispatcher) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -122,7 +122,7 @@ class MediaViewerPresenterTest { fun `present - download media failure then retry with success scenario`() = runTest { val mediaLoader = FakeMediaLoader() val mediaActions = FakeLocalMediaActions() - val presenter = aMediaViewerPresenter(mediaLoader, mediaActions) + val presenter = createMediaViewerPresenter(mediaLoader, mediaActions) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -147,7 +147,7 @@ class MediaViewerPresenterTest { } } - private fun aMediaViewerPresenter( + private fun createMediaViewerPresenter( mediaLoader: FakeMediaLoader, localMediaActions: FakeLocalMediaActions, snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt index 8244562622..94b971f300 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt @@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.report.ReportMessageEvents import io.element.android.features.messages.impl.report.ReportMessagePresenter import io.element.android.libraries.architecture.Async -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index b87fe16aad..3f86ed8b80 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.textcomposer import android.net.Uri +import androidx.compose.runtime.remember import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine @@ -32,7 +33,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.media.FakeLocalMediaFactory import io.element.android.libraries.core.mimetype.MimeTypes -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService @@ -102,7 +103,6 @@ class MessageComposerPresenterTest { assertThat(initialState.showAttachmentSourcePicker).isFalse() assertThat(initialState.canShareLocation).isTrue() assertThat(initialState.attachmentsState).isEqualTo(AttachmentsState.None) - assertThat(initialState.canSendMessage).isFalse() } } @@ -132,13 +132,9 @@ class MessageComposerPresenterTest { skipItems(1) val initialState = awaitItem() initialState.richTextEditorState.setHtml(A_MESSAGE) - val withMessageState = awaitItem() - assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) - assertThat(withMessageState.canSendMessage).isTrue() - withMessageState.richTextEditorState.setHtml("") - val withEmptyMessageState = awaitItem() - assertThat(withEmptyMessageState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(withEmptyMessageState.canSendMessage).isFalse() + assertThat(initialState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) + initialState.richTextEditorState.setHtml("") + assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("") } } @@ -146,7 +142,8 @@ class MessageComposerPresenterTest { fun `present - change mode to edit`() = runTest { val presenter = createPresenter(this) moleculeFlow(RecompositionMode.Immediate) { - presenter.present() + val state = presenter.present() + remember(state, state.richTextEditorState.messageHtml) { state } }.test { skipItems(1) var state = awaitItem() @@ -156,7 +153,6 @@ class MessageComposerPresenterTest { assertThat(state.mode).isEqualTo(mode) state = awaitItem() assertThat(state.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) - assertThat(state.canSendMessage).isTrue() backToNormalMode(state, skipCount = 1) } } @@ -174,7 +170,6 @@ class MessageComposerPresenterTest { state = awaitItem() assertThat(state.mode).isEqualTo(mode) assertThat(state.richTextEditorState.messageHtml).isEqualTo("") - assertThat(state.canSendMessage).isFalse() backToNormalMode(state) } } @@ -192,7 +187,6 @@ class MessageComposerPresenterTest { state = awaitItem() assertThat(state.mode).isEqualTo(mode) assertThat(state.richTextEditorState.messageHtml).isEqualTo("") - assertThat(state.canSendMessage).isFalse() backToNormalMode(state) } } @@ -201,18 +195,17 @@ class MessageComposerPresenterTest { fun `present - send message`() = runTest { val presenter = createPresenter(this) moleculeFlow(RecompositionMode.Immediate) { - presenter.present() + val state = presenter.present() + remember(state, state.richTextEditorState.messageHtml) { state } }.test { skipItems(1) val initialState = awaitItem() initialState.richTextEditorState.setHtml(A_MESSAGE) val withMessageState = awaitItem() assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) - assertThat(withMessageState.canSendMessage).isTrue() withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage())) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(messageSentState.canSendMessage).isFalse() waitForPredicate { analyticsService.capturedEvents.size == 1 } assertThat(analyticsService.capturedEvents).containsExactly( Composer( @@ -233,7 +226,8 @@ class MessageComposerPresenterTest { fakeMatrixRoom, ) moleculeFlow(RecompositionMode.Immediate) { - presenter.present() + val state = presenter.present() + remember(state, state.richTextEditorState.messageHtml) { state } }.test { skipItems(1) val initialState = awaitItem() @@ -244,7 +238,6 @@ class MessageComposerPresenterTest { val withMessageState = awaitItem() assertThat(withMessageState.mode).isEqualTo(mode) assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) - assertThat(withMessageState.canSendMessage).isTrue() withMessageState.richTextEditorState.setHtml(ANOTHER_MESSAGE) val withEditedMessageState = awaitItem() assertThat(withEditedMessageState.richTextEditorState.messageHtml).isEqualTo(ANOTHER_MESSAGE) @@ -252,7 +245,6 @@ class MessageComposerPresenterTest { skipItems(1) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(messageSentState.canSendMessage).isFalse() assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE) assertThat(analyticsService.capturedEvents).containsExactly( Composer( @@ -273,7 +265,8 @@ class MessageComposerPresenterTest { fakeMatrixRoom, ) moleculeFlow(RecompositionMode.Immediate) { - presenter.present() + val state = presenter.present() + remember(state, state.richTextEditorState.messageHtml) { state } }.test { skipItems(1) val initialState = awaitItem() @@ -284,7 +277,6 @@ class MessageComposerPresenterTest { val withMessageState = awaitItem() assertThat(withMessageState.mode).isEqualTo(mode) assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) - assertThat(withMessageState.canSendMessage).isTrue() withMessageState.richTextEditorState.setHtml(ANOTHER_MESSAGE) val withEditedMessageState = awaitItem() assertThat(withEditedMessageState.richTextEditorState.messageHtml).isEqualTo(ANOTHER_MESSAGE) @@ -292,7 +284,6 @@ class MessageComposerPresenterTest { skipItems(1) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(messageSentState.canSendMessage).isFalse() assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE) assertThat(analyticsService.capturedEvents).containsExactly( Composer( @@ -323,16 +314,11 @@ class MessageComposerPresenterTest { val state = awaitItem() assertThat(state.mode).isEqualTo(mode) assertThat(state.richTextEditorState.messageHtml).isEqualTo("") - assertThat(state.canSendMessage).isFalse() state.richTextEditorState.setHtml(A_REPLY) - val withMessageState = awaitItem() - assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_REPLY) - assertThat(withMessageState.canSendMessage).isTrue() - withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY.toMessage())) - skipItems(1) + assertThat(state.richTextEditorState.messageHtml).isEqualTo(A_REPLY) + state.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY.toMessage())) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(messageSentState.canSendMessage).isFalse() assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY to A_REPLY) assertThat(analyticsService.capturedEvents).containsExactly( Composer( @@ -703,7 +689,6 @@ class MessageComposerPresenterTest { val normalState = awaitItem() assertThat(normalState.mode).isEqualTo(MessageComposerMode.Normal("")) assertThat(normalState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(normalState.canSendMessage).isFalse() } private fun createPresenter( diff --git a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorContainer.kt b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorContainer.kt new file mode 100644 index 0000000000..421b0c900f --- /dev/null +++ b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorContainer.kt @@ -0,0 +1,79 @@ +/* + * 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.networkmonitor.api.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.statusBars +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * A view that displays a connectivity indicator when the device is offline, passing the padding + * needed to make sure the status bar is not overlapped to its content views. + */ +@Composable +fun ConnectivityIndicatorContainer( + isOnline: Boolean, + modifier: Modifier = Modifier, + content: @Composable (topPadding: Dp) -> Unit = {}, +) { + val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline } + + val statusBarTopPadding = if (LocalInspectionMode.current) { + // Needed to get valid UI previews + 24.dp + } else { + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 6.dp + } + val target = remember(isIndicatorVisible.targetState, statusBarTopPadding) { + if (!isIndicatorVisible.targetState) 0.dp else statusBarTopPadding + } + val animationStateOffset by animateDpAsState( + targetValue = target, + animationSpec = spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = 1.dp, + ), + label = "insets-animation", + ) + + content(animationStateOffset) + + // Display the network indicator with an animation + AnimatedVisibility( + visibleState = isIndicatorVisible, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + Indicator(modifier) + } +} diff --git a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorView.kt b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorView.kt index 5f32d0baa0..45f62f94b6 100644 --- a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorView.kt +++ b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorView.kt @@ -18,44 +18,17 @@ package io.element.android.features.networkmonitor.api.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.spring import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.text.toDp -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.theme.ElementTheme -import io.element.android.libraries.ui.strings.CommonStrings /** * A view that displays a connectivity indicator when the device is offline, adding a default @@ -88,75 +61,6 @@ fun ConnectivityIndicatorView( } } -/** - * A view that displays a connectivity indicator when the device is offline, passing the padding - * needed to make sure the status bar is not overlapped to its content views. - */ -@Composable -fun ConnectivityIndicatorContainer( - isOnline: Boolean, - modifier: Modifier = Modifier, - content: @Composable (topPadding: Dp) -> Unit, -) { - val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline } - - val statusBarTopPadding = if (LocalInspectionMode.current) { - // Needed to get valid UI previews - 24.dp - } else { - WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 6.dp - } - val target = remember(isIndicatorVisible.targetState, statusBarTopPadding) { - if (!isIndicatorVisible.targetState) 0.dp else statusBarTopPadding - } - val animationStateOffset by animateDpAsState( - targetValue = target, - animationSpec = spring( - stiffness = Spring.StiffnessMediumLow, - visibilityThreshold = 1.dp, - ), - label = "insets-animation", - ) - - content(animationStateOffset) - - // Display the network indicator with an animation - AnimatedVisibility( - visibleState = isIndicatorVisible, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - Indicator(modifier) - } -} - -@Composable -private fun Indicator(modifier: Modifier = Modifier) { - Row( - modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.secondaryContainer) - .statusBarsPadding() - .padding(vertical = 6.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - val tint = MaterialTheme.colorScheme.primary - Icon( - resourceId = CommonDrawables.ic_compound_offline, - contentDescription = null, - tint = tint, - modifier = Modifier.size(16.sp.toDp()), - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(CommonStrings.common_offline), - style = ElementTheme.typography.fontBodyMdMedium, - color = tint, - ) - } -} - @Composable private fun StatusBarPaddingSpacer(modifier: Modifier = Modifier) { Spacer(modifier = modifier.statusBarsPadding()) diff --git a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/Indicator.kt b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/Indicator.kt new file mode 100644 index 0000000000..0be8557bed --- /dev/null +++ b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/Indicator.kt @@ -0,0 +1,69 @@ +/* + * 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.networkmonitor.api.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.libraries.designsystem.text.toDp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +internal fun Indicator( + modifier: Modifier = Modifier, +) { + Row( + modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.secondaryContainer) + .statusBarsPadding() + .padding(vertical = 6.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + val tint = MaterialTheme.colorScheme.primary + Icon( + resourceId = CommonDrawables.ic_compound_offline, + contentDescription = null, + tint = tint, + modifier = Modifier.size(16.sp.toDp()), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(CommonStrings.common_offline), + style = ElementTheme.typography.fontBodyMdMedium, + color = tint, + ) + } +} diff --git a/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml b/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml index 22c9d70004..64ab9f57b3 100644 --- a/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml @@ -3,6 +3,7 @@ "手動登入" "使用 QR code 登入" "建立帳號" + "安全地通訊與協作" "歡迎使用有史以來最快的 Element。速度超快,操作簡便。" "Be in your element" diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt index eccaea45fc..0a7a9ad618 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt @@ -26,7 +26,7 @@ data class CreatePollState( val answers: ImmutableList, val pollKind: PollKind, val showConfirmation: Boolean, - val eventSink: (CreatePollEvents) -> Unit = {}, + val eventSink: (CreatePollEvents) -> Unit, ) data class Answer( diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt index 29aa1288ad..c1393e0da0 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt @@ -18,12 +18,13 @@ package io.element.android.features.poll.impl.create import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.poll.PollKind +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf class CreatePollStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - CreatePollState( + aCreatePollState( canCreate = false, canAddAnswer = true, question = "", @@ -34,7 +35,7 @@ class CreatePollStateProvider : PreviewParameterProvider { pollKind = PollKind.Disclosed, showConfirmation = false, ), - CreatePollState( + aCreatePollState( canCreate = true, canAddAnswer = true, question = "What type of food should we have?", @@ -45,7 +46,7 @@ class CreatePollStateProvider : PreviewParameterProvider { showConfirmation = false, pollKind = PollKind.Undisclosed, ), - CreatePollState( + aCreatePollState( canCreate = true, canAddAnswer = true, question = "What type of food should we have?", @@ -56,7 +57,7 @@ class CreatePollStateProvider : PreviewParameterProvider { showConfirmation = true, pollKind = PollKind.Undisclosed, ), - CreatePollState( + aCreatePollState( canCreate = true, canAddAnswer = true, question = "What type of food should we have?", @@ -69,7 +70,7 @@ class CreatePollStateProvider : PreviewParameterProvider { showConfirmation = false, pollKind = PollKind.Undisclosed, ), - CreatePollState( + aCreatePollState( canCreate = true, canAddAnswer = false, question = "Should there be more than 20 answers?", @@ -98,7 +99,7 @@ class CreatePollStateProvider : PreviewParameterProvider { showConfirmation = false, pollKind = PollKind.Undisclosed, ), - CreatePollState( + aCreatePollState( canCreate = true, canAddAnswer = true, question = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + @@ -122,3 +123,22 @@ class CreatePollStateProvider : PreviewParameterProvider { ) ) } + +private fun aCreatePollState( + canCreate: Boolean, + canAddAnswer: Boolean, + question: String, + answers: PersistentList, + showConfirmation: Boolean, + pollKind: PollKind +): CreatePollState { + return CreatePollState( + canCreate = canCreate, + canAddAnswer = canAddAnswer, + question = question, + answers = answers, + showConfirmation = showConfirmation, + pollKind = pollKind, + eventSink = {} + ) +} diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index f3b41859d9..a227d24b8b 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -55,7 +55,6 @@ dependencies { implementation(projects.services.analytics.api) implementation(projects.services.toolbox.api) implementation(libs.datetime) - implementation(libs.accompanist.placeholder) implementation(libs.coil.compose) implementation(libs.androidx.browser) implementation(libs.androidx.datastore.preferences) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt index 7dd67482f8..a16eb72281 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.libraries.designsystem.components.preferences.PreferenceText -import io.element.android.libraries.designsystem.components.preferences.PreferenceView +import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.ui.strings.CommonStrings @@ -33,7 +33,7 @@ fun AboutView( onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { - PreferenceView( + PreferencePage( modifier = modifier, onBackPressed = onBackPressed, title = stringResource(id = CommonStrings.common_about) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt index ee343a26d6..3bbc9a1b71 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt @@ -20,10 +20,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.preferences.impl.R import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch -import io.element.android.libraries.designsystem.components.preferences.PreferenceView -import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -32,23 +33,20 @@ fun AdvancedSettingsView( onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { - PreferenceView( + PreferencePage( modifier = modifier, onBackPressed = onBackPressed, title = stringResource(id = CommonStrings.common_advanced_settings) ) { PreferenceSwitch( title = stringResource(id = CommonStrings.common_rich_text_editor), - // TODO i18n - subtitle = "Disable the rich text editor to type Markdown manually", + subtitle = stringResource(id = R.string.screen_advanced_settings_rich_text_editor_description), isChecked = state.isRichTextEditorEnabled, onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetRichTextEditorEnabled(it)) }, ) PreferenceSwitch( - // TODO i18n - title = "Developer mode", - // TODO i18n - subtitle = "The developer mode activates hidden features. For developers only!", + title = stringResource(id = R.string.screen_advanced_settings_developer_mode), + subtitle = stringResource(id = R.string.screen_advanced_settings_developer_mode_description), isChecked = state.isDeveloperModeEnabled, onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) }, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt index 1d6e977718..751d513385 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesView -import io.element.android.libraries.designsystem.components.preferences.PreferenceView +import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.ui.strings.CommonStrings @@ -32,7 +32,7 @@ fun AnalyticsSettingsView( onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { - PreferenceView( + PreferencePage( modifier = modifier, onBackPressed = onBackPressed, title = stringResource(id = CommonStrings.common_analytics) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index 7c9988f999..c52106cd82 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceText -import io.element.android.libraries.designsystem.components.preferences.PreferenceView +import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.featureflag.ui.FeatureListView @@ -38,7 +38,7 @@ fun DeveloperSettingsView( onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { - PreferenceView( + PreferencePage( modifier = modifier, onBackPressed = onBackPressed, title = stringResource(id = CommonStrings.common_developer_options) @@ -85,7 +85,7 @@ fun DeveloperSettingsView( } @Composable -fun FeatureListContent( +private fun FeatureListContent( state: DeveloperSettingsState, modifier: Modifier = Modifier ) { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingView.kt index ce1ab2ab6e..15fd6e46b7 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingView.kt @@ -139,7 +139,7 @@ fun ConfigureTracingView( } @Composable -fun CrateListContent( +private fun CrateListContent( state: ConfigureTracingState, modifier: Modifier = Modifier ) { @@ -178,7 +178,7 @@ private fun TargetAndLogLevelListView( } @Composable -fun TargetAndLogLevelView( +private fun TargetAndLogLevelView( target: Target, logLevel: LogLevel, onLogLevelChange: (LogLevel) -> Unit, @@ -197,7 +197,7 @@ fun TargetAndLogLevelView( } @Composable -fun LogLevelDropdownMenu( +private fun LogLevelDropdownMenu( logLevel: LogLevel, onLogLevelChange: (LogLevel) -> Unit, modifier: Modifier = Modifier, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt index 2c82a8a957..0844480697 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt @@ -38,7 +38,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.components.preferences.PreferenceText -import io.element.android.libraries.designsystem.components.preferences.PreferenceView +import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.Button @@ -67,7 +67,7 @@ fun NotificationSettingsView( else -> Unit } } - PreferenceView( + PreferencePage( modifier = modifier, onBackPressed = onBackPressed, title = stringResource(id = CommonStrings.screen_notification_settings_title) @@ -79,7 +79,7 @@ fun NotificationSettingsView( onContinueClicked = { state.eventSink(NotificationSettingsEvents.FixConfigurationMismatch) }, onDismissError = { state.eventSink(NotificationSettingsEvents.ClearConfigurationMismatchError) }, ) - NotificationSettingsState.MatrixSettings.Uninitialized -> return@PreferenceView + NotificationSettingsState.MatrixSettings.Uninitialized -> return@PreferencePage is NotificationSettingsState.MatrixSettings.Valid -> NotificationSettingsContentView( matrixSettings = state.matrixSettings, systemSettings = state.appSettings, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt index 4cc95af71f..3c2c27ac5c 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt @@ -22,7 +22,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory -import io.element.android.libraries.designsystem.components.preferences.PreferenceView +import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.ui.strings.CommonStrings @@ -42,7 +42,7 @@ fun EditDefaultNotificationSettingView( } else { CommonStrings.screen_notification_settings_group_chats } - PreferenceView( + PreferencePage( modifier = modifier, onBackPressed = onBackPressed, title = stringResource(id = title) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index dac7ae3204..200785e03d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -27,8 +27,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.features.logout.api.LogoutPreferencePresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildType -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher -import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt index accede5d6d..f61ea5890d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt @@ -17,7 +17,7 @@ package io.element.android.features.preferences.impl.root import io.element.android.features.logout.api.LogoutPreferenceState -import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.user.MatrixUser data class PreferencesRootState( diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt index 860c738687..467ca4c6d6 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt @@ -17,7 +17,7 @@ package io.element.android.features.preferences.impl.root import io.element.android.features.logout.api.aLogoutPreferenceState -import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.ui.strings.CommonStrings fun aPreferencesRootState() = PreferencesRootState( diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index c473e763b6..2999ddea94 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -31,15 +31,15 @@ import androidx.compose.ui.unit.dp import io.element.android.features.logout.api.LogoutPreferenceView import io.element.android.features.preferences.impl.user.UserPreferences import io.element.android.libraries.designsystem.components.preferences.PreferenceText -import io.element.android.libraries.designsystem.components.preferences.PreferenceView +import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.designsystem.utils.SnackbarHost -import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost +import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.MatrixUserProvider import io.element.android.libraries.theme.ElementTheme @@ -64,7 +64,7 @@ fun PreferencesRootView( val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) // Include pref from other modules - PreferenceView( + PreferencePage( modifier = modifier, onBackPressed = onBackPressed, title = stringResource(id = CommonStrings.common_settings), @@ -151,7 +151,7 @@ fun PreferencesRootView( } @Composable -fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) { +private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) { PreferenceText( title = stringResource(id = CommonStrings.common_developer_options), iconResourceId = CommonDrawables.ic_developer_mode, diff --git a/features/preferences/impl/src/main/res/values-cs/translations.xml b/features/preferences/impl/src/main/res/values-cs/translations.xml index 26b082e386..b26a3d9113 100644 --- a/features/preferences/impl/src/main/res/values-cs/translations.xml +++ b/features/preferences/impl/src/main/res/values-cs/translations.xml @@ -1,5 +1,8 @@ + "Vývojářský režim" + "Povolením získáte přístup k funkcím a funkcím pro vývojáře." + "Vypněte editor formátovaného textu pro ruční zadání Markdown." "Zobrazované jméno" "Vaše zobrazované jméno" "Došlo k neznámé chybě a informace nelze změnit." diff --git a/features/preferences/impl/src/main/res/values-de/translations.xml b/features/preferences/impl/src/main/res/values-de/translations.xml index 6e26c5ddf1..3992b93ab7 100644 --- a/features/preferences/impl/src/main/res/values-de/translations.xml +++ b/features/preferences/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,7 @@ + "Entwickler-Modus" + "Deaktiviere den Rich-Text-Editor, um Markdown manuell einzugeben." "Anzeigename" "Dein Anzeigename" "Ein unbekannter Fehler ist aufgetreten und die Informationen konnten nicht geändert werden." diff --git a/features/preferences/impl/src/main/res/values-fr/translations.xml b/features/preferences/impl/src/main/res/values-fr/translations.xml index 392b28c785..3466ecd630 100644 --- a/features/preferences/impl/src/main/res/values-fr/translations.xml +++ b/features/preferences/impl/src/main/res/values-fr/translations.xml @@ -1,5 +1,8 @@ + "Mode développeur" + "Activer pour pouvoir accéder aux fonctionnalités destinées aux développeurs." + "Désactivez l’éditeur de texte enrichi pour saisir manuellement du Markdown." "Pseudonyme" "Votre pseudonyme" "Une erreur inconnue s’est produite et les informations n’ont pas pu être modifiées." diff --git a/features/preferences/impl/src/main/res/values-sk/translations.xml b/features/preferences/impl/src/main/res/values-sk/translations.xml index 46631c4eb3..392999558e 100644 --- a/features/preferences/impl/src/main/res/values-sk/translations.xml +++ b/features/preferences/impl/src/main/res/values-sk/translations.xml @@ -1,5 +1,8 @@ + "Vývojársky režim" + "Umožniť prístup k možnostiam a funkciám pre vývojárov." + "Vypnite rozšírený textový editor na ručné písanie Markdown." "Zobrazované meno" "Vaše zobrazované meno" "Vyskytla sa neznáma chyba a informácie nebolo možné zmeniť." diff --git a/features/preferences/impl/src/main/res/values-zh-rTW/translations.xml b/features/preferences/impl/src/main/res/values-zh-rTW/translations.xml index f1cb358027..046e820a51 100644 --- a/features/preferences/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/preferences/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,5 +1,6 @@ + "開發者模式" "顯示名稱" "您的顯示名稱" "無法更新個人檔案" diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml index f01ae2b5e1..b94db7a565 100644 --- a/features/preferences/impl/src/main/res/values/localazy.xml +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -1,5 +1,8 @@ + "Developer mode" + "Enable to have access to features and functionality for developers." + "Disable the rich text editor to type Markdown manually." "Display name" "Your display name" "An unknown error was encountered and the information couldn\'t be changed." diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt index 70d15c7a71..cc2c0a7035 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt @@ -32,7 +32,7 @@ import kotlin.time.Duration.Companion.milliseconds class NotificationSettingsPresenterTests { @Test fun `present - ensures initial state is correct`() = runTest { - val presenter = aNotificationPresenter() + val presenter = createNotificationSettingsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -58,7 +58,7 @@ class NotificationSettingsPresenterTests { @Test fun `present - default group notification mode changed`() = runTest { val notificationSettingsService = FakeNotificationSettingsService() - val presenter = aNotificationPresenter(notificationSettingsService) + val presenter = createNotificationSettingsPresenter(notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -77,7 +77,7 @@ class NotificationSettingsPresenterTests { @Test fun `present - notification settings mismatched`() = runTest { val notificationSettingsService = FakeNotificationSettingsService() - val presenter = aNotificationPresenter(notificationSettingsService) + val presenter = createNotificationSettingsPresenter(notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -108,7 +108,7 @@ class NotificationSettingsPresenterTests { initialEncryptedOneToOneDefaultMode = RoomNotificationMode.ALL_MESSAGES, initialOneToOneDefaultMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY ) - val presenter = aNotificationPresenter(notificationSettingsService) + val presenter = createNotificationSettingsPresenter(notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -125,7 +125,7 @@ class NotificationSettingsPresenterTests { @Test fun `present - set notifications enabled`() = runTest { - val presenter = aNotificationPresenter() + val presenter = createNotificationSettingsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -145,7 +145,7 @@ class NotificationSettingsPresenterTests { @Test fun `present - set call notifications enabled`() = runTest { - val presenter = aNotificationPresenter() + val presenter = createNotificationSettingsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -167,7 +167,7 @@ class NotificationSettingsPresenterTests { @Test fun `present - set atRoom notifications enabled`() = runTest { - val presenter = aNotificationPresenter() + val presenter = createNotificationSettingsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -187,7 +187,7 @@ class NotificationSettingsPresenterTests { } } - private fun aNotificationPresenter( + private fun createNotificationSettingsPresenter( notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService() ) : NotificationSettingsPresenter { val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index 2237f717bd..e9eaf5ef13 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.logout.impl.DefaultLogoutPreferencePresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.meta.BuildType -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_AVATAR_URL diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDetectionView.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDetectionView.kt index 914afd4421..3d49d637e5 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDetectionView.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDetectionView.kt @@ -49,7 +49,7 @@ fun CrashDetectionView( } @Composable -fun CrashDetectionContent( +private fun CrashDetectionContent( onNoClicked: () -> Unit = { }, onYesClicked: () -> Unit = { }, onDismiss: () -> Unit = { }, diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/detection/RageshakeDetectionView.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/detection/RageshakeDetectionView.kt index db0a305c72..88fe5dceb2 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/detection/RageshakeDetectionView.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/detection/RageshakeDetectionView.kt @@ -81,7 +81,7 @@ private fun TakeScreenshot( } @Composable -fun RageshakeDialogContent( +private fun RageshakeDialogContent( onNoClicked: () -> Unit = { }, onDisableClicked: () -> Unit = { }, onYesClicked: () -> Unit = { }, diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt index 70033df079..d886819690 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt @@ -43,7 +43,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.components.preferences.PreferenceRow import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch -import io.element.android.libraries.designsystem.components.preferences.PreferenceView +import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground @@ -72,7 +72,7 @@ fun BugReportView( } Box(modifier = modifier) { - PreferenceView( + PreferencePage( title = stringResource(id = CommonStrings.common_report_a_bug), onBackPressed = onBackPressed ) { diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/LogFormatter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/LogFormatter.kt index e0dc1a5fbb..45c2d0a65b 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/LogFormatter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/LogFormatter.kt @@ -28,9 +28,9 @@ import java.util.logging.LogRecord internal class LogFormatter : Formatter() { override fun format(r: LogRecord): String { - if (!mIsTimeZoneSet) { + if (!isTimeZoneSet) { DATE_FORMAT.timeZone = TimeZone.getTimeZone("UTC") - mIsTimeZoneSet = true + isTimeZoneSet = true } val thrown = r.thrown @@ -59,6 +59,6 @@ internal class LogFormatter : Formatter() { // private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss*SSSZZZZ", Locale.US) - private var mIsTimeZoneSet = false + private var isTimeZoneSet = false } } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index 46ccc7fc56..a693189d3c 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -101,10 +101,10 @@ class DefaultBugReporter @Inject constructor( } // the pending bug report call - private var mBugReportCall: Call? = null + private var bugReportCall: Call? = null // boolean to cancel the bug report - private val mIsCancelled = false + private val isCancelled = false /* val adapter = MatrixJsonParser.getMoshi() @@ -151,7 +151,7 @@ class DefaultBugReporter @Inject constructor( listener: BugReporterListener? ) { // enumerate files to delete - val mBugReportFiles: MutableList = ArrayList() + val bugReportFiles: MutableList = ArrayList() try { @@ -172,7 +172,7 @@ class DefaultBugReporter @Inject constructor( val files = getLogFiles() files.mapNotNullTo(gzippedFiles) { f -> when { - mIsCancelled -> null + isCancelled -> null f.extension == "gz" -> f else -> compressFile(f) } @@ -180,7 +180,7 @@ class DefaultBugReporter @Inject constructor( files.deleteAllExceptMostRecent() } - if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) { + if (!isCancelled && (withCrashLogs || withDevicesLogs)) { val gzippedLogcat = saveLogCat(false) if (null != gzippedLogcat) { @@ -215,7 +215,7 @@ class DefaultBugReporter @Inject constructor( val userId = sessionData?.userId ?: "undefined" var olmVersion = "undefined" - if (!mIsCancelled) { + if (!isCancelled) { val text = when (reportType) { ReportType.BUG_REPORT -> bugDescription ReportType.SUGGESTION -> "[Suggestion] $bugDescription" @@ -268,7 +268,7 @@ class DefaultBugReporter @Inject constructor( } } - mBugReportFiles.addAll(gzippedFiles) + bugReportFiles.addAll(gzippedFiles) if (gzippedFiles.isNotEmpty() && !uploadedSomeLogs) { serverError = "Couldn't upload any logs, please retry." @@ -336,8 +336,8 @@ class DefaultBugReporter @Inject constructor( 0 } - if (mIsCancelled && null != mBugReportCall) { - mBugReportCall!!.cancel() + if (isCancelled && null != bugReportCall) { + bugReportCall!!.cancel() } Timber.v("## onWrite() : $percentage%") @@ -360,8 +360,8 @@ class DefaultBugReporter @Inject constructor( // trigger the request try { - mBugReportCall = okHttpClient.get().newCall(request) - response = mBugReportCall!!.execute() + bugReportCall = okHttpClient.get().newCall(request) + response = bugReportCall!!.execute() responseCode = response.code } catch (e: CancellationException) { throw e @@ -423,11 +423,11 @@ class DefaultBugReporter @Inject constructor( } withContext(coroutineDispatchers.main) { - mBugReportCall = null + bugReportCall = null if (null != listener) { try { - if (mIsCancelled) { + if (isCancelled) { listener.onUploadCancelled() } else if (null == serverError) { listener.onUploadSucceed(reportURL) @@ -443,7 +443,7 @@ class DefaultBugReporter @Inject constructor( } } finally { // delete the generated files when the bug report process has finished - for (file in mBugReportFiles) { + for (file in bugReportFiles) { file.safeDelete() } } diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 44c711a371..3bf38d4fa7 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -64,7 +64,7 @@ dependencies { testImplementation(projects.libraries.usersearch.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.tests.testutils) - testImplementation(projects.features.leaveroom.fake) + testImplementation(projects.features.leaveroom.test) 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 675ef7de60..71eff4e9e6 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 @@ -68,7 +68,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( data object InviteMembers : NavTarget @Parcelize - object RoomNotificationSettings : NavTarget + data object RoomNotificationSettings : NavTarget @Parcelize data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget 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 c580e6d677..bbbcc64da6 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 @@ -18,6 +18,7 @@ package io.element.android.features.roomdetails.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember @@ -80,7 +81,7 @@ fun aRoomDetailsState() = RoomDetailsState( canShowNotificationSettings = true, roomType = RoomDetailsType.Room, roomMemberDetailsState = null, - leaveRoomState = LeaveRoomState(), + leaveRoomState = aLeaveRoomState(), roomNotificationSettings = RoomNotificationSettings(mode = RoomNotificationMode.MUTE, isDefault = false), 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 f13f91e27d..c6b90e5cc6 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 @@ -183,7 +183,7 @@ fun RoomDetailsView( @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun RoomDetailsTopBar( +private fun RoomDetailsTopBar( goBack: () -> Unit, onActionClicked: (RoomDetailsAction) -> Unit, showEdit: Boolean, @@ -220,7 +220,7 @@ internal fun RoomDetailsTopBar( } @Composable -internal fun MainActionsSection(state: RoomDetailsState, onShareRoom: () -> Unit, modifier: Modifier = Modifier) { +private fun MainActionsSection(state: RoomDetailsState, onShareRoom: () -> Unit, modifier: Modifier = Modifier) { Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { val roomNotificationSettings = state.roomNotificationSettings if (state.canShowNotificationSettings && roomNotificationSettings != null) { @@ -252,7 +252,7 @@ internal fun MainActionsSection(state: RoomDetailsState, onShareRoom: () -> Unit } @Composable -internal fun RoomHeaderSection( +private fun RoomHeaderSection( avatarUrl: String?, roomId: String, roomName: String, @@ -289,7 +289,7 @@ internal fun RoomHeaderSection( } @Composable -internal fun TopicSection( +private fun TopicSection( roomTopic: RoomTopicState, onActionClicked: (RoomDetailsAction) -> Unit, modifier: Modifier = Modifier @@ -315,7 +315,7 @@ internal fun TopicSection( } @Composable -internal fun NotificationSection( +private fun NotificationSection( isDefaultMode: Boolean, openRoomNotificationSettings: () -> Unit, modifier: Modifier = Modifier @@ -336,14 +336,14 @@ internal fun NotificationSection( } @Composable -internal fun MembersSection( +private fun MembersSection( memberCount: Long, openRoomMemberList: () -> Unit, modifier: Modifier = Modifier, ) { PreferenceCategory(modifier = modifier) { PreferenceText( - title = stringResource(R.string.screen_room_details_people_title), + title = stringResource(CommonStrings.common_people), icon = Icons.Outlined.Person, currentValue = memberCount.toString(), onClick = openRoomMemberList, @@ -352,7 +352,7 @@ internal fun MembersSection( } @Composable -internal fun InviteSection( +private fun InviteSection( invitePeople: () -> Unit, modifier: Modifier = Modifier, ) { @@ -366,7 +366,7 @@ internal fun InviteSection( } @Composable -internal fun SecuritySection(modifier: Modifier = Modifier) { +private fun SecuritySection(modifier: Modifier = Modifier) { PreferenceCategory(title = stringResource(R.string.screen_room_details_security_title), modifier = modifier) { PreferenceText( title = stringResource(R.string.screen_room_details_encryption_enabled_title), @@ -377,7 +377,7 @@ internal fun SecuritySection(modifier: Modifier = Modifier) { } @Composable -internal fun OtherActionsSection(onLeaveRoom: () -> Unit, modifier: Modifier = Modifier) { +private fun OtherActionsSection(onLeaveRoom: () -> Unit, modifier: Modifier = Modifier) { PreferenceCategory(showDivider = false, modifier = modifier) { PreferenceText( title = stringResource(R.string.screen_room_details_leave_room_title), diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserDialogs.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserDialogs.kt new file mode 100644 index 0000000000..ddbff5b6a9 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserDialogs.kt @@ -0,0 +1,77 @@ +/* + * 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.blockuser + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import io.element.android.features.roomdetails.impl.R +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog + +@Composable +fun BlockUserDialogs(state: RoomMemberDetailsState) { + when (state.displayConfirmationDialog) { + null -> Unit + RoomMemberDetailsState.ConfirmationDialog.Block -> { + BlockConfirmationDialog( + onBlockAction = { + state.eventSink( + RoomMemberDetailsEvents.BlockUser( + needsConfirmation = false + ) + ) + }, + onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) } + ) + } + RoomMemberDetailsState.ConfirmationDialog.Unblock -> { + UnblockConfirmationDialog( + onUnblockAction = { + state.eventSink( + RoomMemberDetailsEvents.UnblockUser( + needsConfirmation = false + ) + ) + }, + onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) } + ) + } + } +} + +@Composable +private fun BlockConfirmationDialog(onBlockAction: () -> Unit, onDismiss: () -> Unit) { + ConfirmationDialog( + title = stringResource(R.string.screen_dm_details_block_user), + content = stringResource(R.string.screen_dm_details_block_alert_description), + submitText = stringResource(R.string.screen_dm_details_block_alert_action), + onSubmitClicked = onBlockAction, + onDismiss = onDismiss + ) +} + +@Composable +private fun UnblockConfirmationDialog(onUnblockAction: () -> Unit, onDismiss: () -> Unit) { + ConfirmationDialog( + title = stringResource(R.string.screen_dm_details_unblock_user), + content = stringResource(R.string.screen_dm_details_unblock_alert_description), + submitText = stringResource(R.string.screen_dm_details_unblock_alert_action), + onSubmitClicked = onUnblockAction, + onDismiss = onDismiss + ) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserSection.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserSection.kt index cccf682c9c..c21e8520b3 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserSection.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserSection.kt @@ -27,7 +27,6 @@ import io.element.android.features.roomdetails.impl.members.details.RoomMemberDe import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceText @@ -85,44 +84,3 @@ private fun PreferenceBlockUser( ) } } - -@Composable -internal fun BlockUserDialogs(state: RoomMemberDetailsState) { - when (state.displayConfirmationDialog) { - null -> Unit - RoomMemberDetailsState.ConfirmationDialog.Block -> { - BlockConfirmationDialog( - onBlockAction = { state.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) }, - onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) } - ) - } - RoomMemberDetailsState.ConfirmationDialog.Unblock -> { - UnblockConfirmationDialog( - onUnblockAction = { state.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)) }, - onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) } - ) - } - } -} - -@Composable -internal fun BlockConfirmationDialog(onBlockAction: () -> Unit, onDismiss: () -> Unit) { - ConfirmationDialog( - title = stringResource(R.string.screen_dm_details_block_user), - content = stringResource(R.string.screen_dm_details_block_alert_description), - submitText = stringResource(R.string.screen_dm_details_block_alert_action), - onSubmitClicked = onBlockAction, - onDismiss = onDismiss - ) -} - -@Composable -internal fun UnblockConfirmationDialog(onUnblockAction: () -> Unit, onDismiss: () -> Unit) { - ConfirmationDialog( - title = stringResource(R.string.screen_dm_details_unblock_user), - content = stringResource(R.string.screen_dm_details_unblock_alert_description), - submitText = stringResource(R.string.screen_dm_details_unblock_alert_action), - onSubmitClicked = onUnblockAction, - onDismiss = onDismiss - ) -} 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 index 9a2ceb7c4b..16436debf0 100644 --- 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 @@ -19,15 +19,14 @@ 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 = {}, + val canInvite: Boolean, + val searchQuery: String, + val searchResults: SearchBarResultState>, + val selectedUsers: ImmutableList, + val isSearchActive: Boolean, + val eventSink: (RoomInviteMembersEvents) -> Unit, ) data class InvitableUser( 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 index 00e9496c2a..f44d518fb5 100644 --- 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 @@ -18,20 +18,22 @@ 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.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import kotlinx.collections.immutable.ImmutableList 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( + aRoomInviteMembersState(), + aRoomInviteMembersState(canInvite = true, selectedUsers = aMatrixUserList().toImmutableList()), + aRoomInviteMembersState(isSearchActive = true, searchQuery = "some query"), + aRoomInviteMembersState(isSearchActive = true, searchQuery = "some query", selectedUsers = aMatrixUserList().toImmutableList()), + aRoomInviteMembersState(isSearchActive = true, searchQuery = "some query", searchResults = SearchBarResultState.NoResults()), + aRoomInviteMembersState( isSearchActive = true, canInvite = true, searchQuery = "some query", @@ -48,7 +50,7 @@ internal class RoomInviteMembersStateProvider : PreviewParameterProvider> = SearchBarResultState.NotSearching(), + selectedUsers: ImmutableList = persistentListOf(), + isSearchActive: Boolean = false, +): RoomInviteMembersState { + return RoomInviteMembersState( + canInvite = canInvite, + searchQuery = searchQuery, + searchResults = searchResults, + selectedUsers = selectedUsers, + isSearchActive = isSearchActive, + eventSink = {}, + ) +} 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 index d100fa0fd3..ef37ee26bb 100644 --- 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 @@ -111,7 +111,7 @@ fun RoomInviteMembersView( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun RoomInviteMembersTopBar( +private fun RoomInviteMembersTopBar( canSend: Boolean, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index 5023c24cdc..991ac60054 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -203,7 +203,7 @@ private fun RoomMemberListTopBar( modifier = modifier, title = { Text( - text = stringResource(R.string.screen_room_details_people_title), + text = stringResource(CommonStrings.common_people), style = ElementTheme.typography.aliasScreenTitle, ) }, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt index 0d3423e179..957db2233e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt @@ -23,7 +23,7 @@ data class RoomMemberDetailsState( val userName: String?, val avatarUrl: String?, val isBlocked: Async, - val displayConfirmationDialog: ConfirmationDialog? = null, + val displayConfirmationDialog: ConfirmationDialog?, val isCurrentUser: Boolean, val eventSink: (RoomMemberDetailsEvents) -> Unit ) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt index 6883b20898..b14b0e3634 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt @@ -37,6 +37,7 @@ fun aRoomMemberDetailsState() = RoomMemberDetailsState( userName = "Daniel", avatarUrl = null, isBlocked = Async.Success(false), + displayConfirmationDialog = null, isCurrentUser = false, eventSink = {}, ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt index ec0a1781e0..bd65e706b4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt @@ -16,49 +16,27 @@ package io.element.android.features.roomdetails.impl.members.details -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ChatBubbleOutline import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection -import io.element.android.libraries.designsystem.components.avatar.Avatar -import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.components.button.MainActionButton -import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory -import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.theme.ElementTheme -import io.element.android.libraries.ui.strings.CommonStrings @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable @@ -103,51 +81,9 @@ fun RoomMemberDetailsView( } } +/* @Composable -internal fun RoomMemberHeaderSection( - avatarUrl: String?, - userId: String, - userName: String?, - modifier: Modifier = Modifier -) { - Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Box(modifier = Modifier.size(70.dp)) { - Avatar( - avatarData = AvatarData(userId, userName, avatarUrl, AvatarSize.UserHeader), - modifier = Modifier.fillMaxSize() - ) - } - Spacer(modifier = Modifier.height(24.dp)) - if (userName != null) { - Text(text = userName, style = ElementTheme.typography.fontHeadingLgBold) - Spacer(modifier = Modifier.height(6.dp)) - } - Text( - text = userId, - style = ElementTheme.typography.fontBodyLgRegular, - color = MaterialTheme.colorScheme.secondary, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - textAlign = TextAlign.Center, - ) - Spacer(Modifier.height(40.dp)) - } -} - -@Composable -internal fun RoomMemberMainActionsSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) { - Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { - MainActionButton( - title = stringResource(CommonStrings.action_share), - iconResourceId = CommonDrawables.ic_compound_share_android, - onClick = onShareUser - ) - } -} - -@Composable -internal fun SendMessageSection(onSendMessage: () -> Unit, modifier: Modifier = Modifier) { +private fun SendMessageSection(onSendMessage: () -> Unit, modifier: Modifier = Modifier) { PreferenceCategory(modifier = modifier) { PreferenceText( title = stringResource(CommonStrings.action_send_message), @@ -156,6 +92,7 @@ internal fun SendMessageSection(onSendMessage: () -> Unit, modifier: Modifier = ) } } + */ @PreviewWithLargeHeight @Composable diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberHeaderSection.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberHeaderSection.kt new file mode 100644 index 0000000000..26412577b8 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberHeaderSection.kt @@ -0,0 +1,69 @@ +/* + * 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.members.details + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun RoomMemberHeaderSection( + avatarUrl: String?, + userId: String, + userName: String?, + modifier: Modifier = Modifier +) { + Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Box(modifier = Modifier.size(70.dp)) { + Avatar( + avatarData = AvatarData(userId, userName, avatarUrl, AvatarSize.UserHeader), + modifier = Modifier.fillMaxSize() + ) + } + Spacer(modifier = Modifier.height(24.dp)) + if (userName != null) { + Text(text = userName, style = ElementTheme.typography.fontHeadingLgBold) + Spacer(modifier = Modifier.height(6.dp)) + } + Text( + text = userId, + style = ElementTheme.typography.fontBodyLgRegular, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(40.dp)) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberMainActionsSection.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberMainActionsSection.kt new file mode 100644 index 0000000000..edd352f228 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberMainActionsSection.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.members.details + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.designsystem.components.button.MainActionButton +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun RoomMemberMainActionsSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) { + Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + MainActionButton( + title = stringResource(CommonStrings.action_share), + iconResourceId = CommonDrawables.ic_compound_share_android, + onClick = onShareUser + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt index 7c8206f6a2..8234582c9c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt @@ -108,7 +108,7 @@ fun RoomNotificationSettingsView( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun RoomNotificationSettingsTopBar( +private fun RoomNotificationSettingsTopBar( modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, ) { @@ -125,7 +125,7 @@ fun RoomNotificationSettingsTopBar( } @Composable -fun RoomNotificationSettingsOptions( +private fun RoomNotificationSettingsOptions( selected: RoomNotificationMode?, enabled: Boolean, modifier: Modifier = Modifier, diff --git a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml index 14db6a6d91..d8a76b50d7 100644 --- a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml @@ -9,6 +9,9 @@ "編輯聊天室" "無法更新聊天室" "訊息已加密" + "載入通知設定時發生錯誤。" + "無法關閉聊天室通知,請再試一次。" + "無法開啟聊天室通知,請再試一次。" "邀請夥伴" "自訂" "預設" 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 a6ffa0a289..5ab79d2cee 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 @@ -22,7 +22,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter -import io.element.android.features.leaveroom.fake.LeaveRoomPresenterFake +import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter import io.element.android.features.roomdetails.impl.RoomDetailsEvent import io.element.android.features.roomdetails.impl.RoomDetailsPresenter import io.element.android.features.roomdetails.impl.RoomDetailsType @@ -59,9 +59,10 @@ class RoomDetailsPresenterTests { @get:Rule val warmUpRule = WarmUpRule() - private fun aRoomDetailsPresenter( + + private fun createRoomDetailsPresenter( room: MatrixRoom, - leaveRoomPresenter: LeaveRoomPresenter = LeaveRoomPresenterFake(), + leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(), dispatchers: CoroutineDispatchers, notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService() ): RoomDetailsPresenter { @@ -88,7 +89,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state is created from room info`() = runTest { val room = aMatrixRoom() - val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -107,7 +108,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state with no room name`() = runTest { val room = aMatrixRoom(name = null) - val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -129,7 +130,7 @@ class RoomDetailsPresenterTests { val roomMembers = listOf(myRoomMember, otherRoomMember) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) } - val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -145,7 +146,7 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenCanInviteResult(Result.success(true)) } - val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -163,7 +164,7 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -178,7 +179,7 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenCanInviteResult(Result.failure(Throwable("Whoops"))) } - val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -196,7 +197,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.failure(Throwable("Whelp"))) givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -225,7 +226,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true)) givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -254,7 +255,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true)) } - val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -275,7 +276,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true)) givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -296,7 +297,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(false)) givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -314,7 +315,7 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -332,7 +333,7 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(false)) } - val presenter = aRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -348,9 +349,9 @@ class RoomDetailsPresenterTests { @Test fun `present - leave room event is passed on to leave room presenter`() = runTest { - val leaveRoomPresenter = LeaveRoomPresenterFake() + val leaveRoomPresenter = FakeLeaveRoomPresenter() val room = aMatrixRoom() - val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers()) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -364,10 +365,10 @@ class RoomDetailsPresenterTests { @Test fun `present - notification mode changes`() = runTest { - val leaveRoomPresenter = LeaveRoomPresenterFake() + val leaveRoomPresenter = FakeLeaveRoomPresenter() val notificationSettingsService = FakeNotificationSettingsService() val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) - val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) + val presenter = createRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -383,10 +384,10 @@ class RoomDetailsPresenterTests { @Test fun `present - mute room notifications`() = runTest { - val leaveRoomPresenter = LeaveRoomPresenterFake() + val leaveRoomPresenter = FakeLeaveRoomPresenter() val notificationSettingsService = FakeNotificationSettingsService(initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) - val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) + val presenter = createRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -401,13 +402,13 @@ class RoomDetailsPresenterTests { @Test fun `present - unmute room notifications`() = runTest { - val leaveRoomPresenter = LeaveRoomPresenterFake() + val leaveRoomPresenter = FakeLeaveRoomPresenter() val notificationSettingsService = FakeNotificationSettingsService( initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, initialEncryptedGroupDefaultMode = RoomNotificationMode.ALL_MESSAGES ) val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) - val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) + val presenter = createRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt index 03a47026dd..eb89c40a24 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt @@ -77,7 +77,7 @@ class RoomDetailsEditPresenterTest { unmockkAll() } - private fun aRoomDetailsEditPresenter( + private fun createRoomDetailsEditPresenter( room: MatrixRoom, permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), ): RoomDetailsEditPresenter { @@ -92,7 +92,7 @@ class RoomDetailsEditPresenterTest { @Test fun `present - initial state is created from room info`() = runTest { val room = aMatrixRoom(avatarUrl = AN_AVATAR_URL) - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -119,7 +119,7 @@ class RoomDetailsEditPresenterTest { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(false)) givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.failure(Throwable("Oops"))) } - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -145,7 +145,7 @@ class RoomDetailsEditPresenterTest { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true)) givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.failure(Throwable("Oops"))) } - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -171,7 +171,7 @@ class RoomDetailsEditPresenterTest { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.failure(Throwable("Oops"))) givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true)) } - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -193,7 +193,7 @@ class RoomDetailsEditPresenterTest { @Test fun `present - updates state in response to changes`() = runTest { val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL) - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -239,7 +239,7 @@ class RoomDetailsEditPresenterTest { fakePickerProvider.givenResult(anotherAvatarUri) - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -260,7 +260,7 @@ class RoomDetailsEditPresenterTest { fakePickerProvider.givenResult(anotherAvatarUri) val fakePermissionsPresenter = FakePermissionsPresenter() - val presenter = aRoomDetailsEditPresenter( + val presenter = createRoomDetailsEditPresenter( room = room, permissionsPresenter = fakePermissionsPresenter, ) @@ -293,7 +293,7 @@ class RoomDetailsEditPresenterTest { fakePickerProvider.givenResult(roomAvatarUri) - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -345,7 +345,7 @@ class RoomDetailsEditPresenterTest { fakePickerProvider.givenResult(roomAvatarUri) - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -395,7 +395,7 @@ class RoomDetailsEditPresenterTest { fun `present - save changes room details if different`() = runTest { val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL) - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -420,7 +420,7 @@ class RoomDetailsEditPresenterTest { fun `present - save doesn't change room details if they're the same trimmed`() = runTest { val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL) - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -444,7 +444,7 @@ class RoomDetailsEditPresenterTest { fun `present - save doesn't change topic if it was unset and is now blank`() = runTest { val room = aMatrixRoom(topic = null, name = "Name", avatarUrl = AN_AVATAR_URL) - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -467,7 +467,7 @@ class RoomDetailsEditPresenterTest { fun `present - save doesn't change name if it's now empty`() = runTest { val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL) - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -492,7 +492,7 @@ class RoomDetailsEditPresenterTest { givenPickerReturnsFile() - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -517,7 +517,7 @@ class RoomDetailsEditPresenterTest { fakePickerProvider.givenResult(anotherAvatarUri) fakeMediaPreProcessor.givenResult(Result.failure(Throwable("Oh no"))) - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -583,7 +583,7 @@ class RoomDetailsEditPresenterTest { givenSetTopicResult(Result.failure(Throwable("!"))) } - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -602,7 +602,7 @@ class RoomDetailsEditPresenterTest { } private suspend fun saveAndAssertFailure(room: MatrixRoom, event: RoomDetailsEditEvents) { - val presenter = aRoomDetailsEditPresenter(room) + val presenter = createRoomDetailsEditPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index f5a08ba860..17817ebc91 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -53,7 +53,6 @@ dependencies { implementation(projects.features.networkmonitor.api) implementation(projects.features.leaveroom.api) implementation(projects.services.analytics.api) - implementation(libs.accompanist.placeholder) api(projects.features.roomlist.api) ksp(libs.showkase.processor) @@ -70,5 +69,5 @@ dependencies { testImplementation(projects.features.invitelist.test) testImplementation(projects.features.networkmonitor.test) testImplementation(projects.tests.testutils) - testImplementation(projects.features.leaveroom.fake) + testImplementation(projects.features.leaveroom.test) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index e069634d49..5238144755 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -33,8 +33,8 @@ import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher -import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.getCurrentUser diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index c555afeca7..f0dfe8e3a9 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -19,7 +19,7 @@ package io.element.android.features.roomlist.impl import androidx.compose.runtime.Immutable import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 421e243504..2a27777e4a 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -18,11 +18,12 @@ package io.element.android.features.roomlist.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser @@ -58,7 +59,7 @@ internal fun aRoomListState() = RoomListState( invitesState = InvitesState.NoInvites, displaySearchResults = false, contextMenu = RoomListState.ContextMenu.Hidden, - leaveRoomState = LeaveRoomState(), + leaveRoomState = aLeaveRoomState(), eventSink = {} ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index a669c9d707..b01c5fba4d 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -59,8 +59,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.LogCompositions -import io.element.android.libraries.designsystem.utils.SnackbarHost -import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost +import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.RoomId @Composable @@ -124,7 +124,7 @@ fun RoomListView( @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable -fun RoomListContent( +private fun RoomListContent( state: RoomListState, onVerifyClicked: () -> Unit, onRoomClicked: (RoomId) -> Unit, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt index 384777774e..2016cf5a6b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt @@ -85,7 +85,7 @@ internal fun RoomSummaryRow( @OptIn(ExperimentalFoundationApi::class) @Composable -internal fun RoomSummaryRealRow( +private fun RoomSummaryRealRow( room: RoomListRoomSummary, onClick: (RoomListRoomSummary) -> Unit, onLongClick: (RoomListRoomSummary) -> Unit, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchResultView.kt similarity index 99% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt rename to features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchResultView.kt index 07043b5e5d..799e94bdbd 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchResultView.kt @@ -100,7 +100,7 @@ internal fun RoomListSearchResultView( @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable -internal fun RoomListSearchResultContent( +private fun RoomListSearchResultContent( state: RoomListState, onRoomClicked: (RoomId) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index db2f7027c4..fd5c0160fc 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -22,7 +22,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter -import io.element.android.features.leaveroom.fake.LeaveRoomPresenterFake +import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.roomlist.impl.datasource.FakeInviteDataSource @@ -34,7 +34,7 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter import io.element.android.libraries.matrix.api.MatrixClient @@ -316,7 +316,7 @@ class RoomListPresenterTests { @Test fun `present - leave room calls into leave room presenter`() = runTest { - val leaveRoomPresenter = LeaveRoomPresenterFake() + val leaveRoomPresenter = FakeLeaveRoomPresenter() val scope = CoroutineScope(coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter(leaveRoomPresenter = leaveRoomPresenter, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { @@ -364,7 +364,7 @@ class RoomListPresenterTests { networkMonitor: NetworkMonitor = FakeNetworkMonitor(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), inviteStateDataSource: InviteStateDataSource = FakeInviteDataSource(), - leaveRoomPresenter: LeaveRoomPresenter = LeaveRoomPresenterFake(), + leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(), lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply { givenFormat(A_FORMATTED_DATE) }, diff --git a/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFakeModule.kt b/features/signedout/api/build.gradle.kts similarity index 57% rename from features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFakeModule.kt rename to features/signedout/api/build.gradle.kts index b20b88db1c..d2815315f3 100644 --- a/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFakeModule.kt +++ b/features/signedout/api/build.gradle.kts @@ -14,17 +14,15 @@ * limitations under the License. */ -package io.element.android.features.leaveroom.fake - -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module -import io.element.android.features.leaveroom.api.LeaveRoomPresenter -import io.element.android.libraries.di.SessionScope - -@Module -@ContributesTo(SessionScope::class) -interface LeaveRoomPresenterFakeModule { - @Binds - fun leaveRoomPresenter(leaveRoomPresenter: LeaveRoomPresenterFake): LeaveRoomPresenter +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.signedout.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) } diff --git a/features/signedout/api/src/main/kotlin/io/element/android/features/signedout/api/SignedOutEntryPoint.kt b/features/signedout/api/src/main/kotlin/io/element/android/features/signedout/api/SignedOutEntryPoint.kt new file mode 100644 index 0000000000..7a156998d1 --- /dev/null +++ b/features/signedout/api/src/main/kotlin/io/element/android/features/signedout/api/SignedOutEntryPoint.kt @@ -0,0 +1,37 @@ +/* + * 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.signedout.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.matrix.api.core.SessionId + +interface SignedOutEntryPoint : FeatureEntryPoint { + + data class Params( + val sessionId: SessionId, + ) + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun params(params: Params): NodeBuilder + fun build(): Node + } +} + diff --git a/features/leaveroom/fake/build.gradle.kts b/features/signedout/impl/build.gradle.kts similarity index 68% rename from features/leaveroom/fake/build.gradle.kts rename to features/signedout/impl/build.gradle.kts index 19a057d5ba..0255ac3ca1 100644 --- a/features/leaveroom/fake/build.gradle.kts +++ b/features/signedout/impl/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -17,10 +17,12 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) + alias(libs.plugins.ksp) + id("kotlin-parcelize") } android { - namespace = "io.element.android.features.leaveroom.fake" + namespace = "io.element.android.features.signedout.impl" } anvil { @@ -30,15 +32,22 @@ anvil { dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) + api(projects.features.signedout.api) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) - api(projects.features.leaveroom.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) - testImplementation(libs.coroutines.core) testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.sessionStorage.implMemory) + testImplementation(projects.tests.testutils) + + ksp(libs.showkase.processor) } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPoint.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPoint.kt new file mode 100644 index 0000000000..59b19f041e --- /dev/null +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/DefaultSignedOutEntryPoint.kt @@ -0,0 +1,46 @@ +/* + * 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.signedout.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.signedout.api.SignedOutEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultSignedOutEntryPoint @Inject constructor() : SignedOutEntryPoint { + + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): SignedOutEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : SignedOutEntryPoint.NodeBuilder { + + override fun params(params: SignedOutEntryPoint.Params): SignedOutEntryPoint.NodeBuilder { + plugins += SignedOutNode.Inputs(params.sessionId) + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvents.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvents.kt new file mode 100644 index 0000000000..a2057226a4 --- /dev/null +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvents.kt @@ -0,0 +1,21 @@ +/* + * 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.signedout.impl + +sealed interface SignedOutEvents { + data object SignInAgain : SignedOutEvents +} diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutNode.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutNode.kt new file mode 100644 index 0000000000..381daa7278 --- /dev/null +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutNode.kt @@ -0,0 +1,54 @@ +/* + * 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.signedout.impl + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.core.SessionId + +@ContributesNode(AppScope::class) +class SignedOutNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: SignedOutPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val sessionId: SessionId, + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val presenter = presenterFactory.create(inputs.sessionId.value) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + SignedOutView( + state = state, + modifier = modifier + ) + } +} diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt new file mode 100644 index 0000000000..a93d22253d --- /dev/null +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.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.features.signedout.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.sessionstorage.api.SessionStore +import kotlinx.coroutines.launch + +class SignedOutPresenter @AssistedInject constructor( + @Assisted private val sessionId: String, /* Cannot inject SessionId */ + private val sessionStore: SessionStore, + private val buildMeta: BuildMeta, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(sessionId: String): SignedOutPresenter + } + + @Composable + override fun present(): SignedOutState { + val sessions by sessionStore.sessionsFlow().collectAsState(initial = emptyList()) + val signedOutSession by remember { + derivedStateOf { sessions.firstOrNull { it.userId == sessionId } } + } + val coroutineScope = rememberCoroutineScope() + + fun handleEvents(event: SignedOutEvents) { + when (event) { + SignedOutEvents.SignInAgain -> coroutineScope.launch { + sessionStore.removeSession(sessionId) + } + } + } + + return SignedOutState( + appName = buildMeta.applicationName, + signedOutSession = signedOutSession, + eventSink = ::handleEvents + ) + } +} diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt new file mode 100644 index 0000000000..eff0ab6d5b --- /dev/null +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.signedout.impl + +import io.element.android.libraries.sessionstorage.api.SessionData + +// Do not use default value, so no member get forgotten in the presenters. +data class SignedOutState( + val appName: String, + val signedOutSession: SessionData?, + val eventSink: (SignedOutEvents) -> Unit, +) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt new file mode 100644 index 0000000000..0182e87cf3 --- /dev/null +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt @@ -0,0 +1,54 @@ +/* + * 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.signedout.impl + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.sessionstorage.api.LoginType +import io.element.android.libraries.sessionstorage.api.SessionData + +open class SignedOutStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSignedOutState(), + // Add other states here + ) +} + +fun aSignedOutState() = SignedOutState( + appName = "AppName", + signedOutSession = aSessionData(), + eventSink = {}, +) + +fun aSessionData( + sessionId: SessionId = SessionId("@alice:server.org"), + isTokenValid: Boolean = false, +): SessionData { + return SessionData( + userId = sessionId.value, + deviceId = "aDeviceId", + accessToken = "anAccessToken", + refreshToken = "aRefreshToken", + homeserverUrl = "aHomeserverUrl", + oidcData = null, + slidingSyncProxy = null, + loginTimestamp = null, + isTokenValid = isTokenValid, + loginType = LoginType.UNKNOWN, + ) +} diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt new file mode 100644 index 0000000000..63845ead90 --- /dev/null +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt @@ -0,0 +1,153 @@ +/* + * 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.signedout.impl + +import androidx.activity.compose.BackHandler +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.runtime.Composable +import androidx.compose.ui.BiasAlignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem +import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun SignedOutView( + state: SignedOutState, + modifier: Modifier = Modifier, +) { + BackHandler(onBack = { state.eventSink(SignedOutEvents.SignInAgain) }) + HeaderFooterPage( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding(), + header = { SignedOutHeader(state) }, + content = { SignedOutContent() }, + footer = { + SignedOutFooter( + onSignInAgain = { state.eventSink(SignedOutEvents.SignInAgain) }, + ) + } + ) +} + +@Composable +private fun SignedOutHeader(state: SignedOutState) { + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), + title = stringResource(id = R.string.screen_signed_out_title), + subTitle = stringResource(id = R.string.screen_signed_out_subtitle, state.appName), + iconImageVector = Icons.Filled.AccountCircle, + iconTint = ElementTheme.colors.iconSecondary, + ) +} + +@Composable +private fun SignedOutContent( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = BiasAlignment( + horizontalBias = 0f, + verticalBias = -0.4f + ) + ) { + InfoListOrganism( + items = persistentListOf( + InfoListItem( + message = stringResource(id = R.string.screen_signed_out_reason_1), + iconComposable = { Icon(R.drawable.ic_lock_outline) }, + ), + InfoListItem( + message = stringResource(id = R.string.screen_signed_out_reason_2), + iconComposable = { Icon(R.drawable.ic_devices) }, + ), + InfoListItem( + message = stringResource(id = R.string.screen_signed_out_reason_3), + iconComposable = { Icon(R.drawable.ic_do_disturb_alt) }, + ), + ), + textStyle = ElementTheme.typography.fontBodyMdMedium, + iconTint = ElementTheme.colors.textPrimary, + backgroundColor = ElementTheme.colors.temporaryColorBgSpecial + ) + } +} + +@Composable +private fun Icon( + @DrawableRes iconResourceId: Int, + modifier: Modifier = Modifier, +) { + Icon( + modifier = modifier + .size(20.dp), + resourceId = iconResourceId, + contentDescription = null, + tint = ElementTheme.colors.iconSecondary, + ) +} + +@Composable +private fun SignedOutFooter( + modifier: Modifier = Modifier, + onSignInAgain: () -> Unit, +) { + ButtonColumnMolecule( + modifier = modifier, + ) { + Button( + text = stringResource(id = CommonStrings.action_sign_in_again), + onClick = onSignInAgain, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@PreviewsDayNight +@Composable +internal fun SignedOutViewPreview( + @PreviewParameter(SignedOutStateProvider::class) state: SignedOutState, +) = ElementPreview { + SignedOutView( + state = state, + ) +} diff --git a/features/signedout/impl/src/main/res/drawable/ic_devices.xml b/features/signedout/impl/src/main/res/drawable/ic_devices.xml new file mode 100644 index 0000000000..e01de99a1d --- /dev/null +++ b/features/signedout/impl/src/main/res/drawable/ic_devices.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/features/signedout/impl/src/main/res/drawable/ic_do_disturb_alt.xml b/features/signedout/impl/src/main/res/drawable/ic_do_disturb_alt.xml new file mode 100644 index 0000000000..2ac90bd377 --- /dev/null +++ b/features/signedout/impl/src/main/res/drawable/ic_do_disturb_alt.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/features/signedout/impl/src/main/res/drawable/ic_lock_outline.xml b/features/signedout/impl/src/main/res/drawable/ic_lock_outline.xml new file mode 100644 index 0000000000..51831bf3c1 --- /dev/null +++ b/features/signedout/impl/src/main/res/drawable/ic_lock_outline.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/features/signedout/impl/src/main/res/values/localazy.xml b/features/signedout/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..f70b0042d5 --- /dev/null +++ b/features/signedout/impl/src/main/res/values/localazy.xml @@ -0,0 +1,8 @@ + + + "You’ve changed your password on another session" + "You have deleted the session from another session" + "Your server’s administrator has invalidated your access" + "You might have been signed out for one of the reasons listed below. Please sign in again to continue using %s." + "You’re signed out" + diff --git a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt new file mode 100644 index 0000000000..208d21154e --- /dev/null +++ b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt @@ -0,0 +1,86 @@ +/* + * 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.signedout.impl + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class SignedOutPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + private val appName = "AppName" + + @Test + fun `present - initial state`() = runTest { + val aSessionData = aSessionData() + val sessionStore = InMemorySessionStore().apply { + storeData(aSessionData) + } + val presenter = createSignedOutPresenter(sessionStore = sessionStore) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.appName).isEqualTo(appName) + assertThat(initialState.signedOutSession).isEqualTo(aSessionData) + } + } + + @Test + fun `present - sign in again`() = runTest { + val aSessionData = aSessionData() + val sessionStore = InMemorySessionStore().apply { + storeData(aSessionData) + } + val presenter = createSignedOutPresenter(sessionStore = sessionStore) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.signedOutSession).isEqualTo(aSessionData) + assertThat(sessionStore.getAllSessions()).isNotEmpty() + initialState.eventSink(SignedOutEvents.SignInAgain) + assertThat(awaitItem().signedOutSession).isNull() + assertThat(sessionStore.getAllSessions()).isEmpty() + } + } + + private fun createSignedOutPresenter( + sessionId: SessionId = A_SESSION_ID, + sessionStore: SessionStore = InMemorySessionStore(), + ): SignedOutPresenter { + return SignedOutPresenter( + sessionId = sessionId.value, + sessionStore = sessionStore, + buildMeta = aBuildMeta(applicationName = appName), + ) + } +} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt index ef9a6e63d4..9ee44a790a 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt @@ -42,8 +42,8 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text @@ -51,7 +51,6 @@ import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings -import kotlinx.coroutines.sync.Mutex import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as FlowStep @Composable @@ -74,7 +73,6 @@ fun VerifySelfSessionView( val buttonsVisible by remember(verificationFlowStep) { derivedStateOf { verificationFlowStep != FlowStep.AwaitingOtherDeviceResponse && verificationFlowStep != FlowStep.Completed } } - Mutex() HeaderFooterPage( modifier = modifier, header = { @@ -91,7 +89,7 @@ fun VerifySelfSessionView( } @Composable -internal fun HeaderContent(verificationFlowStep: FlowStep, modifier: Modifier = Modifier) { +private fun HeaderContent(verificationFlowStep: FlowStep, modifier: Modifier = Modifier) { val iconResourceId = when (verificationFlowStep) { FlowStep.Initial -> R.drawable.ic_verification_devices FlowStep.Canceled -> R.drawable.ic_verification_warning @@ -100,7 +98,7 @@ internal fun HeaderContent(verificationFlowStep: FlowStep, modifier: Modifier = } val titleTextId = when (verificationFlowStep) { FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_title - FlowStep.Canceled -> R.string.screen_session_verification_cancelled_title + FlowStep.Canceled -> CommonStrings.common_verification_cancelled FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_to_accept_title FlowStep.Ready, is FlowStep.Verifying, FlowStep.Completed -> R.string.screen_session_verification_compare_emojis_title } @@ -120,7 +118,7 @@ internal fun HeaderContent(verificationFlowStep: FlowStep, modifier: Modifier = } @Composable -internal fun Content(flowState: FlowStep, modifier: Modifier = Modifier) { +private fun Content(flowState: FlowStep, modifier: Modifier = Modifier) { Column(modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) { when (flowState) { FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit @@ -131,14 +129,14 @@ internal fun Content(flowState: FlowStep, modifier: Modifier = Modifier) { } @Composable -internal fun ContentWaiting(modifier: Modifier = Modifier) { +private fun ContentWaiting(modifier: Modifier = Modifier) { Row(modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { CircularProgressIndicator() } } @Composable -internal fun ContentVerifying(verificationFlowStep: FlowStep.Verifying, modifier: Modifier = Modifier) { +private fun ContentVerifying(verificationFlowStep: FlowStep.Verifying, modifier: Modifier = Modifier) { // We want each row to have up to 4 emojis val rows = verificationFlowStep.emojiList.chunked(4) Column(modifier = modifier.fillMaxWidth()) { @@ -157,7 +155,7 @@ internal fun ContentVerifying(verificationFlowStep: FlowStep.Verifying, modifier } @Composable -internal fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifier) { +private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifier) { Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) { Text( text = emoji.code, @@ -175,7 +173,7 @@ internal fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifi } @Composable -internal fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) { +private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) { val verificationViewState = screenState.verificationFlowStep val eventSink = screenState.eventSink @@ -190,7 +188,7 @@ internal fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) R.string.screen_session_verification_they_match } } - FlowStep.Ready -> R.string.screen_session_verification_positive_button_ready + FlowStep.Ready -> CommonStrings.action_start else -> null } val negativeButtonTitle = when (verificationViewState) { diff --git a/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml b/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml index 8b054ac408..14de424bcc 100644 --- a/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml @@ -4,7 +4,7 @@ "確認顯示在其他工作階段上的表情符號是否和下方的相同。" "比對表情符號" "新的工作階段已完成驗證。它能夠存取您的加密訊息,而其他使用者會將它視為可信任的。" - "為了存取被加密的歷史訊息,請證明這是您本人。" + "為了存取被加密的歷史訊息,您需要證明這是您本人。" "開啟一個現存的工作階段" "重新嘗試驗證" "我準備好了" diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt index eee6c51a07..403324816f 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt @@ -40,7 +40,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Initial state is received`() = runTest { - val presenter = createPresenter() + val presenter = createVerifySelfSessionPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -51,7 +51,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Handles requestVerification`() = runTest { val service = FakeSessionVerificationService() - val presenter = createPresenter(service) + val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -62,7 +62,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Handles startSasVerification`() = runTest { val service = FakeSessionVerificationService() - val presenter = createPresenter(service) + val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -81,7 +81,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Cancelation on initial state does nothing`() = runTest { - val presenter = createPresenter() + val presenter = createVerifySelfSessionPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -96,7 +96,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - A fail in the flow cancels it`() = runTest { val service = FakeSessionVerificationService() - val presenter = createPresenter(service) + val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -113,7 +113,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Canceling the flow once it's verifying cancels it`() = runTest { val service = FakeSessionVerificationService() - val presenter = createPresenter(service) + val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -127,7 +127,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - When verifying, if we receive another challenge we ignore it`() = runTest { val service = FakeSessionVerificationService() - val presenter = createPresenter(service) + val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -140,7 +140,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Restart after cancelation returns to requesting verification`() = runTest { val service = FakeSessionVerificationService() - val presenter = createPresenter(service) + val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -162,7 +162,7 @@ class VerifySelfSessionPresenterTests { val service = FakeSessionVerificationService().apply { givenEmojiList(emojis) } - val presenter = createPresenter(service) + val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -176,7 +176,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - When verification is declined, the flow is canceled`() = runTest { val service = FakeSessionVerificationService() - val presenter = createPresenter(service) + val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -210,7 +210,9 @@ class VerifySelfSessionPresenterTests { return state } - private fun createPresenter(service: FakeSessionVerificationService = FakeSessionVerificationService()): VerifySelfSessionPresenter { + private fun createVerifySelfSessionPresenter( + service: FakeSessionVerificationService = FakeSessionVerificationService() + ): VerifySelfSessionPresenter { return VerifySelfSessionPresenter(service, VerifySelfSessionStateMachine(service)) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9f8af860fb..9f9ae6ddc3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,26 +3,25 @@ [versions] # Project -android_gradle_plugin = "8.1.1" +android_gradle_plugin = "8.1.2" kotlin = "1.9.10" ksp = "1.9.10-1.0.13" molecule = "1.2.1" # AndroidX -material = "1.9.0" core = "1.12.0" datastore = "1.0.0" constraintlayout = "2.1.4" constraintlayout_compose = "1.0.1" recyclerview = "1.3.1" lifecycle = "2.6.2" -activity = "1.7.2" +activity = "1.8.0" startup = "1.1.1" media3 = "1.1.1" browser = "1.6.0" # Compose -compose_bom = "2023.09.01" +compose_bom = "2023.10.00" composecompiler = "1.5.3" # Coroutines @@ -40,16 +39,16 @@ datetime = "0.4.1" serialization_json = "1.6.0" showkase = "1.0.0-beta18" jsoup = "1.16.1" -appyx = "1.3.0" +appyx = "1.4.0" dependencycheck = "8.4.0" -dependencyanalysis = "1.22.0" +dependencyanalysis = "1.25.0" stem = "2.3.0" sqldelight = "1.5.5" telephoto = "0.6.2" -wysiwyg = "2.12.0" +wysiwyg = "2.14.1" # DI -dagger = "2.48" +dagger = "2.48.1" anvil = "2.4.8-1-8" # Auto service @@ -69,7 +68,6 @@ kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", v google_firebase_bom = "com.google.firebase:firebase-bom:32.3.1" # AndroidX -androidx_material = { module = "com.google.android.material:material", version.ref = "material" } androidx_core = { module = "androidx.core:core", version.ref = "core" } androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "core" } androidx_datastore_preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } @@ -94,21 +92,15 @@ androidx_preference = "androidx.preference:preference:1.2.1" androidx_webkit = "androidx.webkit:webkit:1.8.0" androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" } -# Warning: issue on alpha07, make sure this is working when upgrading -# Context in https://github.com/vector-im/element-x-android/pull/1239#issuecomment-1711500332 -androidx_compose_material3 = "androidx.compose.material3:material3:1.2.0-alpha06" +androidx_compose_material3 = "androidx.compose.material3:material3:1.2.0-alpha09" # Coroutines coroutines_core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } coroutines_test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } # Accompanist -accompanist_animation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanist" } accompanist_permission = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } -accompanist_material = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanist" } accompanist_systemui = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } -accompanist_placeholder = { module = "com.google.accompanist:accompanist-placeholder-material", version.ref = "accompanist" } -accompanist_flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" } # Libraries squareup_seismic = "com.squareup:seismic:1.0.3" @@ -130,6 +122,7 @@ test_uiautomator = "androidx.test.uiautomator:uiautomator:2.2.0" test_junitext = "androidx.test.ext:junit:1.1.5" test_mockk = "io.mockk:mockk:1.13.8" test_barista = "com.adevinta.android:barista:4.3.0" +test_konsist = "com.lemonappdev:konsist:0.13.0" test_hamcrest = "org.hamcrest:hamcrest:2.2" test_orchestrator = "androidx.test:orchestrator:1.4.2" test_turbine = "app.cash.turbine:turbine:1.0.0" @@ -150,14 +143,14 @@ 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.58" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.61" matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } 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" } sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4" -sqlite = "androidx.sqlite:sqlite:2.3.1" +sqlite = "androidx.sqlite:sqlite-ktx:2.3.1" unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1" otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5" vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0" @@ -169,7 +162,7 @@ maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.1" # Analytics posthog = "com.posthog.android:posthog:2.0.3" -sentry = "io.sentry:sentry-android:6.29.0" +sentry = "io.sentry:sentry-android:6.30.0" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:e9cd9adaf18cec52ed851395eb84358b4f9b8d7f" # Emojibase @@ -204,7 +197,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } anvil = { id = "com.squareup.anvil", version.ref = "anvil" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } -ktlint = "org.jlleitschuh.gradle.ktlint:11.6.0" +ktlint = "org.jlleitschuh.gradle.ktlint:11.6.1" dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" } dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" } dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyanalysis" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6c7fa4d465..01f330a93e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=bb09982fdf52718e4c7b25023d10df6d35a5fff969860bdf5a5bd27a3ab27a9e -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +distributionSha256Sum=f2b9ed0faf8472cbe469255ae6c86eddb77076c75191741b4a462f33128dd419 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 0adc8e1a53..1aa94a4269 100755 --- a/gradlew +++ b/gradlew @@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/BackstackNode.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/BackstackNode.kt index ec22c5e21f..deb6d0e63b 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/BackstackNode.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/BackstackNode.kt @@ -19,6 +19,8 @@ package io.element.android.libraries.architecture import androidx.compose.runtime.Stable import com.bumble.appyx.core.children.ChildEntry import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.navigation.model.combined.plus +import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.plugin.Plugin @@ -33,10 +35,11 @@ import com.bumble.appyx.navmodel.backstack.BackStack abstract class BackstackNode( val backstack: BackStack, buildContext: BuildContext, + plugins: List, + val permanentNavModel: PermanentNavModel = PermanentNavModel(emptySet(), null), childKeepMode: ChildEntry.KeepMode = ChildEntry.KeepMode.KEEP, - plugins: List ) : ParentNode( - navModel = backstack, + navModel = backstack + permanentNavModel, buildContext = buildContext, plugins = plugins, childKeepMode = childKeepMode, diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt index 0506126568..af2bca157b 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt @@ -31,6 +31,7 @@ object MimeTypes { const val BadJpg = "image/jpg" const val Jpeg = "image/jpeg" const val Gif = "image/gif" + const val WebP = "image/webp" const val Videos = "video/*" const val Mp4 = "video/mp4" diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/InfoListItemMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt similarity index 98% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/InfoListItemMolecule.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt index 3f23bd24b9..c5c328a610 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/InfoListItemMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.designsystem.atomic.atoms +package io.element.android.libraries.designsystem.atomic.molecules import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListOrganism.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt similarity index 79% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListOrganism.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt index 1bc82eb3b2..3a28344fb7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListOrganism.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.designsystem.atomic.molecules +package io.element.android.libraries.designsystem.atomic.organisms import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Arrangement @@ -27,12 +27,15 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.atomic.atoms.InfoListItemMolecule -import io.element.android.libraries.designsystem.atomic.atoms.InfoListItemPosition +import io.element.android.libraries.designsystem.atomic.molecules.InfoListItemMolecule +import io.element.android.libraries.designsystem.atomic.molecules.InfoListItemPosition +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @Composable fun InfoListOrganism( @@ -84,3 +87,17 @@ data class InfoListItem( val iconVector: ImageVector? = null, val iconComposable: @Composable () -> Unit = {}, ) + +@PreviewsDayNight +@Composable +internal fun InfoListOrganismPreview() = ElementPreview { + val items = persistentListOf( + InfoListItem(message = "A top item"), + InfoListItem(message = "A middle item"), + InfoListItem(message = "A bottom item"), + ) + InfoListOrganism( + items, + backgroundColor = ElementTheme.materialColors.surfaceVariant, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt index ecb92ff004..ff8d4221f6 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt @@ -98,15 +98,14 @@ import androidx.compose.ui.unit.toSize import coil.imageLoader import coil.request.DefaultRequestOptions import coil.request.ImageRequest -import coil.size.Size import com.airbnb.android.showkase.annotation.ShowkaseComposable import com.vanniktech.blurhash.BlurHash import io.element.android.libraries.designsystem.R import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewGroup +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar @@ -129,7 +128,9 @@ object BloomDefaults { * Number of components to use with BlurHash to generate the blur effect. * Larger values mean more detailed blurs. */ - const val HASH_COMPONENTS = 5 + const val HASH_COMPONENTS = 4 + const val ENCODE_SIZE_PX = 20 + const val DECODE_SIZE_PX = 5 /** Default bloom layers. */ @Composable @@ -189,7 +190,11 @@ fun Modifier.bloom( if (hash == null) return@composed this val hashedBitmap = remember(hash) { - BlurHash.decode(hash, BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS)?.asImageBitmap() + BlurHash.decode( + blurHash = hash, + width = BloomDefaults.DECODE_SIZE_PX, + height = BloomDefaults.DECODE_SIZE_PX, + )?.asImageBitmap() } ?: return@composed this val density = LocalDensity.current val pixelSize = remember(blurSize, density) { blurSize.toIntSize(density) } @@ -327,7 +332,6 @@ fun Modifier.avatarBloom( // Request the avatar contents to use as the bloom source val context = LocalContext.current - val density = LocalDensity.current if (avatarData.url != null) { val painterRequest = remember(avatarData) { ImageRequest.Builder(context) @@ -337,7 +341,7 @@ fun Modifier.avatarBloom( // Needed to be able to read pixels from the Bitmap for the hash .allowHardware(false) // Reduce size so it loads faster for large avatars - .size(with(density) { Size(64.dp.roundToPx(), 64.dp.roundToPx()) }) + .size(BloomDefaults.ENCODE_SIZE_PX, BloomDefaults.ENCODE_SIZE_PX) .build() } @@ -349,9 +353,9 @@ fun Modifier.avatarBloom( context.imageLoader.execute(painterRequest).drawable ?: return@withContext val bitmap = (drawable as? BitmapDrawable)?.bitmap ?: return@withContext blurHash = BlurHash.encode( - bitmap, - BloomDefaults.HASH_COMPONENTS, - BloomDefaults.HASH_COMPONENTS + bitmap = bitmap, + componentX = BloomDefaults.HASH_COMPONENTS, + componentY = BloomDefaults.HASH_COMPONENTS, ) } } @@ -371,14 +375,18 @@ fun Modifier.avatarBloom( // There is no URL so we'll generate an avatar with the initials and use that as the bloom source val avatarColors = AvatarColorsProvider.provide(avatarData.id, ElementTheme.isLightTheme) val initialsBitmap = initialsBitmap( - width = avatarData.size.dp, - height = avatarData.size.dp, + width = BloomDefaults.ENCODE_SIZE_PX.toDp(), + height = BloomDefaults.ENCODE_SIZE_PX.toDp(), text = avatarData.initial, textColor = avatarColors.foreground, backgroundColor = avatarColors.background, ) val hash = remember(avatarData, avatarColors) { - BlurHash.encode(initialsBitmap.asAndroidBitmap(), BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS) + BlurHash.encode( + bitmap = initialsBitmap.asAndroidBitmap(), + componentX = BloomDefaults.HASH_COMPONENTS, + componentY = BloomDefaults.HASH_COMPONENTS, + ) } bloom( hash = hash, @@ -541,7 +549,11 @@ internal fun BloomInitialsPreview(@PreviewParameter(InitialsColorStateProvider:: ElementPreview { val avatarColors = AvatarColorsProvider.provide("$color", ElementTheme.isLightTheme) val bitmap = initialsBitmap(text = "F", backgroundColor = avatarColors.background, textColor = avatarColors.foreground) - val hash = BlurHash.encode(bitmap.asAndroidBitmap(), BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS) + val hash = BlurHash.encode( + bitmap = bitmap.asAndroidBitmap(), + componentX = BloomDefaults.HASH_COMPONENTS, + componentY = BloomDefaults.HASH_COMPONENTS, + ) Box( modifier = Modifier .size(256.dp) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BlurHashAsyncImage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BlurHashAsyncImage.kt index 0ddd0b7346..dcfce743c1 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BlurHashAsyncImage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BlurHashAsyncImage.kt @@ -71,7 +71,7 @@ fun BlurHashAsyncImage( } @Composable -fun BlurHashImage( +private fun BlurHashImage( blurHash: String?, modifier: Modifier = Modifier, contentDescription: String? = null, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt index 8061e6a1a5..e82ebfe532 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt @@ -72,7 +72,7 @@ fun ListDialog( } @Composable -internal fun ListDialogContent( +private fun ListDialogContent( listItems: LazyListScope.() -> Unit, onDismissRequest: () -> Unit, onSubmitClicked: () -> Unit, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/MultipleSelectionDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/MultipleSelectionDialog.kt index a697a3c753..5ed79f2c35 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/MultipleSelectionDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/MultipleSelectionDialog.kt @@ -78,7 +78,7 @@ fun MultipleSelectionDialog( } @Composable -internal fun MultipleSelectionDialogContent( +private fun MultipleSelectionDialogContent( options: ImmutableList, confirmButtonTitle: String, onConfirmClicked: (List) -> Unit, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/SingleSelectionDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/SingleSelectionDialog.kt index ee45e05d8b..db9e7faef2 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/SingleSelectionDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/SingleSelectionDialog.kt @@ -74,7 +74,7 @@ fun SingleSelectionDialog( } @Composable -internal fun SingleSelectionDialogContent( +private fun SingleSelectionDialogContent( options: ImmutableList, onOptionSelected: (Int) -> Unit, onDismissRequest: () -> Unit, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCategory.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCategory.kt index 885059cb70..e3036194a4 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCategory.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCategory.kt @@ -52,7 +52,7 @@ fun PreferenceCategory( } @Composable -fun PreferenceCategoryTitle(title: String, modifier: Modifier = Modifier) { +private fun PreferenceCategoryTitle(title: String, modifier: Modifier = Modifier) { Text( modifier = modifier.padding( top = 20.dp, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferencePage.kt similarity index 98% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferencePage.kt index c43fbd3d45..653df4a74d 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferencePage.kt @@ -44,7 +44,7 @@ import io.element.android.libraries.theme.ElementTheme @OptIn(ExperimentalLayoutApi::class) @Composable -fun PreferenceView( +fun PreferencePage( title: String, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, @@ -79,7 +79,7 @@ fun PreferenceView( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun PreferenceTopAppBar( +private fun PreferenceTopAppBar( title: String, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, @@ -104,7 +104,7 @@ fun PreferenceTopAppBar( @PreviewsDayNight @Composable internal fun PreferenceViewPreview() = ElementPreview { - PreferenceView( + PreferencePage( title = "Preference screen" ) { PreferenceCategory( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Blur.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Blur.kt index fb3eb86c96..bb3dbec175 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Blur.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Blur.kt @@ -21,7 +21,6 @@ import android.os.Build import androidx.annotation.ChecksSdkIntAtLeast import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.draw.BlurredEdgeTreatment import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.drawBehind @@ -94,8 +93,8 @@ fun Modifier.blurredShapeShadow( fun Modifier.blurCompat( radius: Dp, edgeTreatment: BlurredEdgeTreatment = BlurredEdgeTreatment.Rectangle -): Modifier = composed { - when { +): Modifier { + return when { radius.value == 0f -> this canUseBlur() -> blur(radius, edgeTreatment) else -> this // Added in case we find a way to make this work on older devices diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt index 3c2d61a76e..70777e0184 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt @@ -16,90 +16,11 @@ package io.element.android.libraries.designsystem.preview -import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.theme.ElementTheme -@Composable -fun ElementPreviewLight( - showBackground: Boolean = true, - content: @Composable () -> Unit -) { - ElementPreview( - darkTheme = false, - showBackground = showBackground, - content = content - ) -} - -@Composable -fun ElementPreviewDark( - showBackground: Boolean = true, - content: @Composable () -> Unit -) { - ElementPreview( - darkTheme = true, - showBackground = showBackground, - content = content - ) -} - -@Composable -@Suppress("ModifierMissing") -fun ElementThemedPreview( - showBackground: Boolean = true, - vertical: Boolean = true, - content: @Composable () -> Unit, -) { - Box( - modifier = Modifier - .background(Color.Gray) - .padding(4.dp) - ) { - if (vertical) { - Column { - ElementPreview( - darkTheme = false, - showBackground = showBackground, - content = content, - ) - Spacer(modifier = Modifier.height(4.dp)) - ElementPreview( - darkTheme = true, - showBackground = showBackground, - content = content - ) - } - } else { - Row { - ElementPreview( - darkTheme = false, - showBackground = showBackground, - content = content, - ) - Spacer(modifier = Modifier.width(4.dp)) - ElementPreview( - darkTheme = true, - showBackground = showBackground, - content = content - ) - } - } - } -} - @Composable @Suppress("ModifierMissing") fun ElementPreview( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewDark.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewDark.kt new file mode 100644 index 0000000000..595b82214d --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewDark.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.preview + +import androidx.compose.runtime.Composable + +@Composable +fun ElementPreviewDark( + showBackground: Boolean = true, + content: @Composable () -> Unit +) { + ElementPreview( + darkTheme = true, + showBackground = showBackground, + content = content + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewLight.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewLight.kt new file mode 100644 index 0000000000..fe1c2aa45a --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewLight.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.preview + +import androidx.compose.runtime.Composable + +@Composable +fun ElementPreviewLight( + showBackground: Boolean = true, + content: @Composable () -> Unit +) { + ElementPreview( + darkTheme = false, + showBackground = showBackground, + content = content + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementThemedPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementThemedPreview.kt new file mode 100644 index 0000000000..0a6b9fd46e --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementThemedPreview.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.preview + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +@Suppress("ModifierMissing") +fun ElementThemedPreview( + showBackground: Boolean = true, + vertical: Boolean = true, + content: @Composable () -> Unit, +) { + Box( + modifier = Modifier + .background(Color.Gray) + .padding(4.dp) + ) { + if (vertical) { + Column { + ElementPreview( + darkTheme = false, + showBackground = showBackground, + content = content, + ) + Spacer(modifier = Modifier.height(4.dp)) + ElementPreview( + darkTheme = true, + showBackground = showBackground, + content = content + ) + } + } else { + Row { + ElementPreview( + darkTheme = false, + showBackground = showBackground, + content = content, + ) + Spacer(modifier = Modifier.width(4.dp)) + ElementPreview( + darkTheme = true, + showBackground = showBackground, + content = content + ) + } + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/DpScale.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/DpScale.kt index c6408b662e..95cf050cc3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/DpScale.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/DpScale.kt @@ -52,7 +52,7 @@ fun Dp.applyScaleUp(): Dp = with(LocalDensity.current) { @Preview @Composable -internal fun DpScalePreview_0_75f() = WithFontScale(0.75f) { +internal fun DpScale_0_75f_Preview() = WithFontScale(0.75f) { ElementPreviewLight { val fontSizeInDp = 16.dp Column( @@ -77,7 +77,7 @@ internal fun DpScalePreview_0_75f() = WithFontScale(0.75f) { @Preview @Composable -internal fun DpScalePreview_1_0f() = WithFontScale(1f) { +internal fun DpScale_1_0f_Preview() = WithFontScale(1f) { ElementPreviewLight { val fontSizeInDp = 16.dp Column( @@ -102,7 +102,7 @@ internal fun DpScalePreview_1_0f() = WithFontScale(1f) { @Preview @Composable -internal fun DpScalePreview_1_5f() = WithFontScale(1.5f) { +internal fun DpScale_1_5f_Preview() = WithFontScale(1.5f) { ElementPreviewLight { val fontSizeInDp = 16.dp Column( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetDragHandle.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetDragHandle.kt new file mode 100644 index 0000000000..63dcf1b653 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetDragHandle.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun BottomSheetDragHandle( + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .height(36.dp) + .background(Color.Transparent) + .fillMaxWidth() + .clip(RectangleShape), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .requiredHeight(72.dp) + .offset(y = 18.dp) + .clip(MaterialTheme.shapes.extraLarge) + .background(MaterialTheme.colorScheme.background) + .border(0.5.dp, ElementTheme.colors.borderDisabled, MaterialTheme.shapes.extraLarge) + ) + + Box( + modifier = Modifier + .width(32.dp) + .height(4.dp) + .background(ElementTheme.colors.iconQuaternary, RoundedCornerShape(2.dp)) + ) + } +} + + +@PreviewsDayNight +@Composable +internal fun BottomSheetDragHandlePreview() = ElementPreview { + BottomSheetDragHandle() +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt index 7e3cc144ac..fce2cb3eff 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt @@ -117,7 +117,7 @@ fun TextButton( ) @Composable -internal fun ButtonInternal( +private fun ButtonInternal( text: String, onClick: () -> Unit, style: ButtonStyle, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSectionHeader.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSectionHeader.kt new file mode 100644 index 0000000000..646c7cc6dd --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSectionHeader.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementThemedPreview +import io.element.android.libraries.designsystem.preview.PreviewGroup +import io.element.android.libraries.theme.ElementTheme + +// Designs: https://www.figma.com/file/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?type=design&node-id=425%3A24208&mode=design&t=G5hCfkLB6GgXDuWe-1 + +/** + * List section header. + * @param title The title of the section. + * @param modifier The modifier to be applied to the section. + * @param hasDivider Whether to show a divider above the section or not. Default is `true`. + * @param description A description for the section. It's empty by default. + */ +@Composable +fun ListSectionHeader( + title: String, + modifier: Modifier = Modifier, + hasDivider: Boolean = true, + description: @Composable () -> Unit = {}, +) { + Column(modifier.fillMaxWidth()) { + if (hasDivider) { + HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) + } + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = title, + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + ) + CompositionLocalProvider( + LocalTextStyle provides ElementTheme.typography.fontBodySmRegular, + LocalContentColor provides ElementTheme.colors.textSecondary, + ) { + description() + } + } + } +} + +@Preview(group = PreviewGroup.ListSections, name = "List section header") +@Composable +internal fun ListSectionHeaderPreview() { + ElementThemedPreview { + ListSectionHeader( + title = "List section", + hasDivider = false, + ) + } +} + +@Preview(group = PreviewGroup.ListSections, name = "List section header with divider") +@Composable +internal fun ListSectionHeaderWithDividerPreview() { + ElementThemedPreview { + ListSectionHeader( + title = "List section", + hasDivider = true, + ) + } +} + +@Preview(group = PreviewGroup.ListSections, name = "List section header with description") +@Composable +internal fun ListSectionHeaderWithDescriptionPreview() { + ElementThemedPreview { + ListSectionHeader( + title = "List section", + description = { + ListSupportingText( + text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more", + contentPadding = ListSupportingTextDefaults.Padding.None, + ) + }, + hasDivider = false, + ) + } +} + +@Preview(group = PreviewGroup.ListSections, name = "List section header with description and divider") +@Composable +internal fun ListSectionHeaderWithDescriptionAndDividerPreview() { + ElementThemedPreview { + ListSectionHeader( + title = "List section", + description = { + ListSupportingText( + text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more", + contentPadding = ListSupportingTextDefaults.Padding.None, + ) + }, + hasDivider = true, + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSupportingText.kt similarity index 69% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSupportingText.kt index 32cc676c43..f76562f0cd 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSupportingText.kt @@ -16,15 +16,10 @@ package io.element.android.libraries.designsystem.theme.components -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.ExperimentalTextApi @@ -40,43 +35,6 @@ import io.element.android.libraries.theme.ElementTheme // Designs: https://www.figma.com/file/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?type=design&node-id=425%3A24208&mode=design&t=G5hCfkLB6GgXDuWe-1 -/** - * List section header. - * @param title The title of the section. - * @param modifier The modifier to be applied to the section. - * @param hasDivider Whether to show a divider above the section or not. Default is `true`. - * @param description A description for the section. It's empty by default. - */ -@Composable -fun ListSectionHeader( - title: String, - modifier: Modifier = Modifier, - hasDivider: Boolean = true, - description: @Composable () -> Unit = {}, -) { - Column(modifier.fillMaxWidth()) { - if (hasDivider) { - HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) - } - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = title, - style = ElementTheme.typography.fontBodyLgMedium, - color = ElementTheme.colors.textPrimary, - ) - CompositionLocalProvider( - LocalTextStyle provides ElementTheme.typography.fontBodySmRegular, - LocalContentColor provides ElementTheme.colors.textSecondary, - ) { - description() - } - } - } -} - /** * List supporting text item. Used to display an explanation in the list with a pre-formatted style. * @param text The text to display. @@ -167,68 +125,6 @@ object ListSupportingTextDefaults { } } -// region: List header previews - -@Preview(group = PreviewGroup.ListSections, name = "List section header") -@Composable -internal fun ListSectionHeaderPreview() { - ElementThemedPreview { - ListSectionHeader( - title = "List section", - hasDivider = false, - ) - } -} - -@Preview(group = PreviewGroup.ListSections, name = "List section header with divider") -@Composable -internal fun ListSectionHeaderWithDividerPreview() { - ElementThemedPreview { - ListSectionHeader( - title = "List section", - hasDivider = true, - ) - } -} - -@Preview(group = PreviewGroup.ListSections, name = "List section header with description") -@Composable -internal fun ListSectionHeaderWithDescriptionPreview() { - ElementThemedPreview { - ListSectionHeader( - title = "List section", - description = { - ListSupportingText( - text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more", - contentPadding = ListSupportingTextDefaults.Padding.None, - ) - }, - hasDivider = false, - ) - } -} - -@Preview(group = PreviewGroup.ListSections, name = "List section header with description and divider") -@Composable -internal fun ListSectionHeaderWithDescriptionAndDividerPreview() { - ElementThemedPreview { - ListSectionHeader( - title = "List section", - description = { - ListSupportingText( - text = "Supporting line text lorem ipsum dolor sit amet, consectetur. Read more", - contentPadding = ListSupportingTextDefaults.Padding.None, - ) - }, - hasDivider = true, - ) - } -} - -// endregion - -// region: List supporting text previews - @Preview(group = PreviewGroup.ListSections, name = "List supporting text - no padding") @Composable internal fun ListSupportingTextNoPaddingPreview() { @@ -298,5 +194,3 @@ internal fun ListSupportingTextCustomPaddingPreview() { } } } - -// endregion 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 8e5bcf6f82..65d156fbbc 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 @@ -193,11 +193,11 @@ sealed interface SearchBarResultState { @Preview(group = PreviewGroup.Search) @Composable -internal fun SearchBarPreviewInactive() = ElementThemedPreview { ContentToPreview() } +internal fun SearchBarInactivePreview() = ElementThemedPreview { ContentToPreview() } @Preview(group = PreviewGroup.Search) @Composable -internal fun SearchBarPreviewActiveEmptyQuery() = ElementThemedPreview { +internal fun SearchBarActiveEmptyQueryPreview() = ElementThemedPreview { ContentToPreview( query = "", active = true, @@ -206,7 +206,7 @@ internal fun SearchBarPreviewActiveEmptyQuery() = ElementThemedPreview { @Preview(group = PreviewGroup.Search) @Composable -internal fun SearchBarPreviewActiveWithQuery() = ElementThemedPreview { +internal fun SearchBarActiveWithQueryPreview() = ElementThemedPreview { ContentToPreview( query = "search term", active = true, @@ -215,7 +215,7 @@ internal fun SearchBarPreviewActiveWithQuery() = ElementThemedPreview { @Preview(group = PreviewGroup.Search) @Composable -internal fun SearchBarPreviewActiveWithQueryNoBackButton() = ElementThemedPreview { +internal fun SearchBarActiveWithQueryNoBackButtonPreview() = ElementThemedPreview { ContentToPreview( query = "search term", active = true, @@ -225,7 +225,7 @@ internal fun SearchBarPreviewActiveWithQueryNoBackButton() = ElementThemedPrevie @Preview(group = PreviewGroup.Search) @Composable -internal fun SearchBarPreviewActiveWithNoResults() = ElementThemedPreview { +internal fun SearchBarActiveWithNoResultsPreview() = ElementThemedPreview { ContentToPreview( query = "search term", active = true, @@ -235,7 +235,7 @@ internal fun SearchBarPreviewActiveWithNoResults() = ElementThemedPreview { @Preview(group = PreviewGroup.Search) @Composable -internal fun SearchBarPreviewActiveWithContent() = ElementThemedPreview { +internal fun SearchBarActiveWithContentPreview() = ElementThemedPreview { ContentToPreview( query = "search term", active = true, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt index e0435ee30e..1b2bfd2188 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt @@ -30,13 +30,13 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup @Preview(group = PreviewGroup.DateTimePickers) @Composable -internal fun DatePickerPreviewLight() { +internal fun DatePickerLightPreview() { ElementPreviewLight { ContentToPreview() } } @Preview(group = PreviewGroup.DateTimePickers) @Composable -internal fun DatePickerPreviewDark() { +internal fun DatePickerDarkPreview() { ElementPreviewDark { ContentToPreview() } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt index d900dd6d8b..9e56a38137 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt @@ -55,7 +55,7 @@ internal fun TimePickerHorizontalPreview() { @OptIn(ExperimentalMaterial3Api::class) @Preview(group = PreviewGroup.DateTimePickers) @Composable -internal fun TimePickerVerticalPreviewLight() { +internal fun TimePickerVerticalLightPreview() { ElementPreviewLight { AlertDialogContent( buttons = { /*TODO*/ }, @@ -77,7 +77,7 @@ internal fun TimePickerVerticalPreviewLight() { @OptIn(ExperimentalMaterial3Api::class) @Preview(group = PreviewGroup.DateTimePickers) @Composable -internal fun TimePickerVerticalPreviewDark() { +internal fun TimePickerVerticalDarkPreview() { val pickerState = rememberTimePickerState( initialHour = 12, initialMinute = 0, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcher.kt similarity index 68% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcher.kt index 9072c4d8a4..9275ea1a76 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcher.kt @@ -14,11 +14,8 @@ * limitations under the License. */ -package io.element.android.libraries.designsystem.utils +package io.element.android.libraries.designsystem.utils.snackbar -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -26,11 +23,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.components.button.ButtonVisuals -import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.Snackbar import kotlinx.coroutines.CancellationException import kotlinx.coroutines.currentCoroutineContext @@ -38,7 +31,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.isActive import kotlinx.coroutines.sync.Mutex -import java.util.concurrent.atomic.AtomicBoolean /** * A global dispatcher of [SnackbarMessage] to be displayed in [Snackbar] via a [SnackbarHostState]. @@ -78,23 +70,6 @@ fun SnackbarDispatcher.collectSnackbarMessageAsState(): State return snackbarMessage.collectAsState(initial = null) } -@Composable -fun SnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) { - androidx.compose.material3.SnackbarHost(hostState, modifier) { data -> - Snackbar( - modifier = Modifier.padding(12.dp), // Add default padding - message = data.visuals.message, - action = data.visuals.actionLabel?.let { ButtonVisuals.Text(it, data::performAction) }, - dismissAction = if (data.visuals.withDismissAction) { - ButtonVisuals.Icon( - IconSource.Resource(CommonDrawables.ic_compound_close), - data::dismiss - ) - } else null, - ) - } -} - /** * Helper method to display a [SnackbarMessage] in a [SnackbarHostState] handling cancellations. */ @@ -127,19 +102,3 @@ fun rememberSnackbarHostState(snackbarMessage: SnackbarMessage?): SnackbarHostSt } return snackbarHostState } - -/** - * A message to be displayed in a [Snackbar]. - * @param messageResId The message to be displayed. - * @param duration The duration of the message. The default value is [SnackbarDuration.Short]. - * @param actionResId The action text to be displayed. The default value is `null`. - * @param isDisplayed Used to track if the current message is already displayed or not. - * @param action The action to be performed when the action is clicked. - */ -data class SnackbarMessage( - @StringRes val messageResId: Int, - val duration: SnackbarDuration = SnackbarDuration.Short, - @StringRes val actionResId: Int? = null, - val isDisplayed: AtomicBoolean = AtomicBoolean(false), - val action: () -> Unit = {}, -) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarHost.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarHost.kt new file mode 100644 index 0000000000..257c23a2b7 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarHost.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.libraries.designsystem.utils.snackbar + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.button.ButtonVisuals +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.Snackbar +import io.element.android.libraries.designsystem.utils.CommonDrawables + +@Composable +fun SnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) { + androidx.compose.material3.SnackbarHost(hostState, modifier) { data -> + Snackbar( + modifier = Modifier.padding(12.dp), // Add default padding + message = data.visuals.message, + action = data.visuals.actionLabel?.let { ButtonVisuals.Text(it, data::performAction) }, + dismissAction = if (data.visuals.withDismissAction) { + ButtonVisuals.Icon( + IconSource.Resource(CommonDrawables.ic_compound_close), + data::dismiss + ) + } else null, + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarMessage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarMessage.kt new file mode 100644 index 0000000000..4c254f9027 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarMessage.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.utils.snackbar + +import androidx.annotation.StringRes +import androidx.compose.material3.SnackbarDuration +import java.util.concurrent.atomic.AtomicBoolean + +/** + * A message to be displayed in a [Snackbar]. + * @param messageResId The message to be displayed. + * @param duration The duration of the message. The default value is [SnackbarDuration.Short]. + * @param actionResId The action text to be displayed. The default value is `null`. + * @param isDisplayed Used to track if the current message is already displayed or not. + * @param action The action to be performed when the action is clicked. + */ +data class SnackbarMessage( + @StringRes val messageResId: Int, + val duration: SnackbarDuration = SnackbarDuration.Short, + @StringRes val actionResId: Int? = null, + val isDisplayed: AtomicBoolean = AtomicBoolean(false), + val action: () -> Unit = {}, +) diff --git a/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcherTests.kt b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcherTests.kt similarity index 97% rename from libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcherTests.kt rename to libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcherTests.kt index 3eb644d800..fbccaa4e75 100644 --- a/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcherTests.kt +++ b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcherTests.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.designsystem.utils +package io.element.android.libraries.designsystem.utils.snackbar import app.cash.turbine.test import com.google.common.truth.Truth.assertThat diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt index 7e8a93ffe7..d041078051 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt @@ -106,12 +106,10 @@ class DefaultRoomLastMessageFormatter @Inject constructor( } private fun processMessageContents(messageContent: MessageContent, senderDisplayName: String, isDmRoom: Boolean): CharSequence? { - val messageType: MessageType = messageContent.type ?: return null - - val internalMessage = when (messageType) { + val internalMessage = when (val messageType: MessageType = messageContent.type) { // Doesn't need a prefix is EmoteMessageType -> { - return "- $senderDisplayName ${messageType.body}" + return "* $senderDisplayName ${messageType.body}" } is TextMessageType -> { messageType.body @@ -132,7 +130,8 @@ class DefaultRoomLastMessageFormatter @Inject constructor( sp.getString(CommonStrings.common_audio) } UnknownMessageType -> { - sp.getString(CommonStrings.common_unsupported_event) + // Display the body as a fallback + messageContent.body } is NoticeMessageType -> { messageType.body diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt index 679f9ce891..c906952bbb 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt @@ -201,11 +201,12 @@ class DefaultRoomLastMessageFormatterTests { is ImageMessageType -> "Image" is FileMessageType -> "File" is LocationMessageType -> "Shared location" - is EmoteMessageType -> "- $senderName ${type.body}" - is TextMessageType, is NoticeMessageType -> body - UnknownMessageType -> "Unsupported event" + is EmoteMessageType -> "* $senderName ${type.body}" + is TextMessageType, + is NoticeMessageType, + UnknownMessageType -> body } - Truth.assertWithMessage("$type was not properly handled").that(result).isEqualTo(expectedResult) + Truth.assertWithMessage("$type was not properly handled for DM").that(result).isEqualTo(expectedResult) } // Verify results of Room mode @@ -217,9 +218,10 @@ class DefaultRoomLastMessageFormatterTests { is ImageMessageType -> "$senderName: Image" is FileMessageType -> "$senderName: File" is LocationMessageType -> "$senderName: Shared location" - is EmoteMessageType -> "- $senderName ${type.body}" - is TextMessageType, is NoticeMessageType -> "$senderName: $body" - UnknownMessageType -> "$senderName: Unsupported event" + is TextMessageType, + is NoticeMessageType, + UnknownMessageType -> "$senderName: $body" + is EmoteMessageType -> "* $senderName ${type.body}" } val shouldCreateAnnotatedString = when (type) { is VideoMessageType -> true @@ -236,7 +238,7 @@ class DefaultRoomLastMessageFormatterTests { .that(result) .isInstanceOf(AnnotatedString::class.java) } - Truth.assertWithMessage("$type was not properly handled").that(string).isEqualTo(expectedResult) + Truth.assertWithMessage("$type was not properly handled for room").that(string).isEqualTo(expectedResult) } } diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 990aee3a93..8d35223986 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -43,4 +43,10 @@ enum class FeatureFlags( title = "Show notification settings", defaultValue = true, ), + VoiceMessages( + key = "feature.voicemessages", + title = "Voice messages", + description = "Send and receive voice messages", + defaultValue = false, + ), } diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt index 82184d510c..7ef10262c9 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt @@ -35,6 +35,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.LocationSharing -> true FeatureFlags.Polls -> true FeatureFlags.NotificationSettings -> true + FeatureFlags.VoiceMessages -> false } } else { false diff --git a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/FeatureListView.kt b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/FeatureListView.kt index 065746c876..6bf4467de8 100644 --- a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/FeatureListView.kt +++ b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/FeatureListView.kt @@ -46,7 +46,7 @@ fun FeatureListView( } @Composable -fun FeaturePreferenceView( +private fun FeaturePreferenceView( feature: FeatureUiModel, onCheckedChange: (Boolean) -> Unit, modifier: Modifier = Modifier diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/Symbol.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/Symbol.kt index 36e8cdc34e..bb40c7dfa9 100644 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/Symbol.kt +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/Symbol.kt @@ -50,7 +50,7 @@ internal class SymbolNode( * @param position the initial symbol position */ public class SymbolState( - position: LatLng = LatLng(0.0, 0.0) + position: LatLng ) { /** * Current position of the symbol. diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index 1ffe07eb6f..5a430f7db5 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -44,4 +44,6 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) + testImplementation(libs.test.robolectric) + testImplementation(projects.tests.testutils) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt index c15153876c..501b40508e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt @@ -18,12 +18,18 @@ package io.element.android.libraries.matrix.api.auth import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.sessionstorage.api.LoggedInState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow interface MatrixAuthenticationService { - fun isLoggedIn(): Flow + fun loggedInStateFlow(): Flow suspend fun getLatestSessionId(): SessionId? + + /** + * Restore a session from a [sessionId]. + * Do not restore anything it the access token is not valid anymore. + */ suspend fun restoreSession(sessionId: SessionId): Result fun getHomeserverDetails(): StateFlow suspend fun setHomeserver(homeserver: String): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index 290a54c502..1abbf80130 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -61,7 +61,10 @@ sealed interface NotificationContent { ) : MessageLike data object RoomRedaction : MessageLike data object Sticker : MessageLike - data class Poll(val question: String) : MessageLike + data class Poll( + val senderId: UserId, + val question: String, + ) : MessageLike } sealed interface StateEvent : NotificationContent { 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 17cb637d80..e14138f837 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 @@ -77,9 +77,9 @@ interface MatrixRoom : Closeable { fun destroy() - fun subscribeToSync() + suspend fun subscribeToSync() - fun unsubscribeFromSync() + suspend fun unsubscribeFromSync() suspend fun userDisplayName(userId: UserId): Result @@ -89,7 +89,7 @@ interface MatrixRoom : Closeable { suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result - suspend fun enterReplyMode(eventId: EventId): Result + suspend fun enterSpecialMode(eventId: EventId?): Result suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt index d98a3a83d2..2121670d6b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomNotificationSettingsState.kt @@ -17,7 +17,7 @@ package io.element.android.libraries.matrix.api.room sealed interface MatrixRoomNotificationSettingsState { - object Unknown : MatrixRoomNotificationSettingsState + data object Unknown : MatrixRoomNotificationSettingsState data class Pending(val prevRoomNotificationSettings: RoomNotificationSettings? = null) : MatrixRoomNotificationSettingsState data class Error(val failure: Throwable, val prevRoomNotificationSettings: RoomNotificationSettings? = null) : MatrixRoomNotificationSettingsState data class Ready(val roomNotificationSettings: RoomNotificationSettings) : MatrixRoomNotificationSettingsState diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt index a3edf80bee..f63953e260 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt @@ -16,13 +16,8 @@ package io.element.android.libraries.matrix.api.timeline.item.event -import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.media.AudioInfo -import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo -import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind @@ -33,37 +28,10 @@ data class MessageContent( val inReplyTo: InReplyTo?, val isEdited: Boolean, val isThreaded: Boolean, - val type: MessageType? + val type: MessageType ) : EventContent -sealed interface InReplyTo { - /** The event details are not loaded yet. We can fetch them. */ - data class NotLoaded(val eventId: EventId) : InReplyTo - - /** The event details are pending to be fetched. We should **not** fetch them again. */ - data object Pending : InReplyTo - - /** The event details are available. */ - data class Ready( - val eventId: EventId, - val content: EventContent, - val senderId: UserId, - val senderDisplayName: String?, - val senderAvatarUrl: String?, - ) : InReplyTo - - /** - * Fetching the event details failed. - * - * We can try to fetch them again **with a proper retry strategy**, but not blindly: - * - * If the reason for the failure is consistent on the server, we'd enter a loop - * where we keep trying to fetch the same event. - * */ - data object Error : InReplyTo -} - -object RedactedContent : EventContent +data object RedactedContent : EventContent data class StickerContent( val body: String, @@ -124,106 +92,4 @@ data class FailedToParseStateContent( val error: String ) : EventContent -object UnknownContent : EventContent - -sealed interface MessageType - -object UnknownMessageType : MessageType - -enum class MessageFormat { - HTML, UNKNOWN -} - -data class FormattedBody( - val format: MessageFormat, - val body: String -) - -data class EmoteMessageType( - val body: String, - val formatted: FormattedBody? -) : MessageType - -data class ImageMessageType( - val body: String, - val source: MediaSource, - val info: ImageInfo? -) : MessageType - -data class LocationMessageType( - val body: String, - val geoUri: String, - val description: String?, -) : MessageType - -data class AudioMessageType( - val body: String, - val source: MediaSource, - val info: AudioInfo? -) : MessageType - -data class VideoMessageType( - val body: String, - val source: MediaSource, - val info: VideoInfo? -) : MessageType - -data class FileMessageType( - val body: String, - val source: MediaSource, - val info: FileInfo? -) : MessageType - -data class NoticeMessageType( - val body: String, - val formatted: FormattedBody? -) : MessageType - -data class TextMessageType( - val body: String, - val formatted: FormattedBody? -) : MessageType - -enum class MembershipChange { - NONE, - ERROR, - JOINED, - LEFT, - BANNED, - UNBANNED, - KICKED, - INVITED, - KICKED_AND_BANNED, - INVITATION_ACCEPTED, - INVITATION_REJECTED, - INVITATION_REVOKED, - KNOCKED, - KNOCK_ACCEPTED, - KNOCK_RETRACTED, - KNOCK_DENIED, - NOT_IMPLEMENTED; -} - -sealed interface OtherState { - data object PolicyRuleRoom : OtherState - data object PolicyRuleServer : OtherState - data object PolicyRuleUser : OtherState - data object RoomAliases : OtherState - data class RoomAvatar(val url: String?) : OtherState - data object RoomCanonicalAlias : OtherState - data object RoomCreate : OtherState - data object RoomEncryption : OtherState - data object RoomGuestAccess : OtherState - data object RoomHistoryVisibility : OtherState - data object RoomJoinRules : OtherState - data class RoomName(val name: String?) : OtherState - data object RoomPinnedEvents : OtherState - data object RoomPowerLevels : OtherState - data object RoomServerAcl : OtherState - data class RoomThirdPartyInvite(val displayName: String?) : OtherState - data object RoomTombstone : OtherState - data class RoomTopic(val topic: String?) : OtherState - data object SpaceChild : OtherState - data object SpaceParent : OtherState - data class Custom(val eventType: String) : OtherState -} +data object UnknownContent : EventContent diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/FormattedBody.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/FormattedBody.kt new file mode 100644 index 0000000000..a11043b200 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/FormattedBody.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.timeline.item.event + +data class FormattedBody( + val format: MessageFormat, + val body: String +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt new file mode 100644 index 0000000000..14a84e2a90 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.timeline.item.event + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId + +sealed interface InReplyTo { + /** The event details are not loaded yet. We can fetch them. */ + data class NotLoaded(val eventId: EventId) : InReplyTo + + /** The event details are pending to be fetched. We should **not** fetch them again. */ + data object Pending : InReplyTo + + /** The event details are available. */ + data class Ready( + val eventId: EventId, + val content: EventContent, + val senderId: UserId, + val senderDisplayName: String?, + val senderAvatarUrl: String?, + ) : InReplyTo + + /** + * Fetching the event details failed. + * + * We can try to fetch them again **with a proper retry strategy**, but not blindly: + * + * If the reason for the failure is consistent on the server, we'd enter a loop + * where we keep trying to fetch the same event. + * */ + data object Error : InReplyTo +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MembershipChange.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MembershipChange.kt new file mode 100644 index 0000000000..8aa8845f23 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MembershipChange.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.timeline.item.event + +enum class MembershipChange { + NONE, + ERROR, + JOINED, + LEFT, + BANNED, + UNBANNED, + KICKED, + INVITED, + KICKED_AND_BANNED, + INVITATION_ACCEPTED, + INVITATION_REJECTED, + INVITATION_REVOKED, + KNOCKED, + KNOCK_ACCEPTED, + KNOCK_RETRACTED, + KNOCK_DENIED, + NOT_IMPLEMENTED; +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageFormat.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageFormat.kt new file mode 100644 index 0000000000..4e88113355 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageFormat.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.timeline.item.event + +enum class MessageFormat { + HTML, UNKNOWN +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt new file mode 100644 index 0000000000..dc06d5c94a --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.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.libraries.matrix.api.timeline.item.event + +import io.element.android.libraries.matrix.api.media.AudioInfo +import io.element.android.libraries.matrix.api.media.FileInfo +import io.element.android.libraries.matrix.api.media.ImageInfo +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.media.VideoInfo + +sealed interface MessageType + +data object UnknownMessageType : MessageType + +data class EmoteMessageType( + val body: String, + val formatted: FormattedBody? +) : MessageType + +data class ImageMessageType( + val body: String, + val source: MediaSource, + val info: ImageInfo? +) : MessageType + +data class LocationMessageType( + val body: String, + val geoUri: String, + val description: String?, +) : MessageType + +data class AudioMessageType( + val body: String, + val source: MediaSource, + val info: AudioInfo? +) : MessageType + +data class VideoMessageType( + val body: String, + val source: MediaSource, + val info: VideoInfo? +) : MessageType + +data class FileMessageType( + val body: String, + val source: MediaSource, + val info: FileInfo? +) : MessageType + +data class NoticeMessageType( + val body: String, + val formatted: FormattedBody? +) : MessageType + +data class TextMessageType( + val body: String, + val formatted: FormattedBody? +) : MessageType diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt new file mode 100644 index 0000000000..2cbfaf76b4 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.timeline.item.event + +sealed interface OtherState { + data object PolicyRuleRoom : OtherState + data object PolicyRuleServer : OtherState + data object PolicyRuleUser : OtherState + data object RoomAliases : OtherState + data class RoomAvatar(val url: String?) : OtherState + data object RoomCanonicalAlias : OtherState + data object RoomCreate : OtherState + data object RoomEncryption : OtherState + data object RoomGuestAccess : OtherState + data object RoomHistoryVisibility : OtherState + data object RoomJoinRules : OtherState + data class RoomName(val name: String?) : OtherState + data object RoomPinnedEvents : OtherState + data object RoomPowerLevels : OtherState + data object RoomServerAcl : OtherState + data class RoomThirdPartyInvite(val displayName: String?) : OtherState + data object RoomTombstone : OtherState + data class RoomTopic(val topic: String?) : OtherState + data object SpaceChild : OtherState + data object SpaceParent : OtherState + data class Custom(val eventType: String) : OtherState +} diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverterTests.kt b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverterTests.kt new file mode 100644 index 0000000000..ef0a586fba --- /dev/null +++ b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverterTests.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.permalink + +import android.net.Uri +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MatrixToConverterTests { + + @Test + fun `converting a matrix-to url does nothing`() { + val url = Uri.parse("https://matrix.to/#/#element-android:matrix.org") + assertThat(MatrixToConverter.convert(url)).isEqualTo(url) + } + + @Test + fun `converting a url with a supported room path returns a matrix-to url`() { + val url = Uri.parse("https://riot.im/develop/#/room/#element-android:matrix.org") + assertThat(MatrixToConverter.convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/#element-android:matrix.org")) + } + + @Test + fun `converting a url with a supported user path returns a matrix-to url`() { + val url = Uri.parse("https://riot.im/develop/#/user/@test:matrix.org") + assertThat(MatrixToConverter.convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/@test:matrix.org")) + } + + @Test + fun `converting a url with a supported group path returns a matrix-to url`() { + val url = Uri.parse("https://riot.im/develop/#/group/+group:matrix.org") + assertThat(MatrixToConverter.convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/+group:matrix.org")) + } + + @Test + fun `converting an unsupported url returns null`() { + val url = Uri.parse("https://element.io/") + assertThat(MatrixToConverter.convert(url)).isNull() + } + +} diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilderTests.kt b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilderTests.kt new file mode 100644 index 0000000000..0a9a03cabb --- /dev/null +++ b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilderTests.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.permalink + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.tests.testutils.assertThrowsInDebug +import io.element.android.tests.testutils.isInDebug +import org.junit.Test + +class PermalinkBuilderTests { + + fun `building a permalink for an invalid user id throws when verifying the id`() { + assertThrowsInDebug { + val userId = UserId("some invalid user id") + PermalinkBuilder.permalinkForUser(userId) + } + } + + fun `building a permalink for an invalid room id throws when verifying the id`() { + assertThrowsInDebug { + val roomId = RoomId("some invalid room id") + PermalinkBuilder.permalinkForRoomId(roomId) + } + } + + @Test + fun `building a permalink for an invalid user id returns failure when not verifying the id`() { + if (!isInDebug()) { + val userId = UserId("some invalid user id") + assertThat(PermalinkBuilder.permalinkForUser(userId).isFailure).isTrue() + } + } + + @Test + fun `building a permalink for an invalid room id returns failure when not verifying the id`() { + if (!isInDebug()) { + val roomId = RoomId("some invalid room id") + assertThat(PermalinkBuilder.permalinkForRoomId(roomId).isFailure).isTrue() + } + } + + @Test + fun `building a permalink for an invalid room alias returns failure`() { + val roomAlias = "an invalid room alias" + assertThat(PermalinkBuilder.permalinkForRoomAlias(roomAlias).isFailure).isTrue() + } + + @Test + fun `building a permalink for a valid user id returns a matrix-to url`() { + val userId = UserId("@user:matrix.org") + assertThat(PermalinkBuilder.permalinkForUser(userId).getOrNull()).isEqualTo("https://matrix.to/#/@user:matrix.org") + } + + @Test + fun `building a permalink for a valid room id returns a matrix-to url`() { + val roomId = RoomId("!aBCdEFG1234:matrix.org") + assertThat(PermalinkBuilder.permalinkForRoomId(roomId).getOrNull()).isEqualTo("https://matrix.to/#/!aBCdEFG1234:matrix.org") + } + + @Test + fun `building a permalink for a valid room alias returns a matrix-to url`() { + val roomAlias = "#room:matrix.org" + assertThat(PermalinkBuilder.permalinkForRoomAlias(roomAlias).getOrNull()).isEqualTo("https://matrix.to/#/#room:matrix.org") + } +} diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTests.kt b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTests.kt new file mode 100644 index 0000000000..74797083c4 --- /dev/null +++ b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTests.kt @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.permalink + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class PermalinkParserTests { + + @Test + fun `parsing an invalid url returns a fallback link`() { + val url = "https://element.io" + assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) + } + + @Test + fun `parsing an invalid url with the right path but no content returns a fallback link`() { + val url = "https://app.element.io/#/user" + assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) + } + + @Test + fun `parsing an invalid url with the right path but empty content returns a fallback link`() { + val url = "https://app.element.io/#/user/" + assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) + } + + @Test + fun `parsing an invalid url with the right path but invalid content returns a fallback link`() { + val url = "https://app.element.io/#/user/some%20user!" + assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) + } + + @Test + fun `parsing a valid user url returns a user link`() { + val url = "https://app.element.io/#/user/@test:matrix.org" + assertThat(PermalinkParser.parse(url)).isEqualTo( + PermalinkData.UserLink( + userId = "@test:matrix.org" + ) + ) + } + + @Test + fun `parsing a valid room id url returns a room link`() { + val url = "https://app.element.io/#/room/!aBCD1234:matrix.org" + assertThat(PermalinkParser.parse(url)).isEqualTo( + PermalinkData.RoomLink( + roomIdOrAlias = "!aBCD1234:matrix.org", + isRoomAlias = false, + eventId = null, + viaParameters = emptyList(), + ) + ) + } + + @Test + fun `parsing a valid room id with event id url returns a room link`() { + val url = "https://app.element.io/#/room/!aBCD1234:matrix.org/$1234567890abcdef:matrix.org" + assertThat(PermalinkParser.parse(url)).isEqualTo( + PermalinkData.RoomLink( + roomIdOrAlias = "!aBCD1234:matrix.org", + isRoomAlias = false, + eventId = "\$1234567890abcdef:matrix.org", + viaParameters = emptyList(), + ) + ) + } + + @Test + fun `parsing a valid room id with and invalid event id url returns a room link with no event id`() { + val url = "https://app.element.io/#/room/!aBCD1234:matrix.org/1234567890abcdef:matrix.org" + assertThat(PermalinkParser.parse(url)).isEqualTo( + PermalinkData.RoomLink( + roomIdOrAlias = "!aBCD1234:matrix.org", + isRoomAlias = false, + eventId = null, + viaParameters = emptyList(), + ) + ) + } + + @Test + fun `parsing a valid room id with event id and via parameters url returns a room link`() { + val url = "https://app.element.io/#/room/!aBCD1234:matrix.org/$1234567890abcdef:matrix.org?via=matrix.org&via=matrix.com" + assertThat(PermalinkParser.parse(url)).isEqualTo( + PermalinkData.RoomLink( + roomIdOrAlias = "!aBCD1234:matrix.org", + isRoomAlias = false, + eventId = "\$1234567890abcdef:matrix.org", + viaParameters = listOf("matrix.org", "matrix.com"), + ) + ) + } + + @Test + fun `parsing a valid room alias url returns a room link`() { + val url = "https://app.element.io/#/room/#element-android:matrix.org" + assertThat(PermalinkParser.parse(url)).isEqualTo( + PermalinkData.RoomLink( + roomIdOrAlias = "#element-android:matrix.org", + isRoomAlias = true, + eventId = null, + viaParameters = emptyList(), + ) + ) + } + + @Test + fun `parsing a url with an invalid signurl returns a fallback link`() { + // This url has no private key + val url = "https://app.element.io/#/room/%21aBCDEF12345%3Amatrix.org" + + "?email=testuser%40element.io" + + "&signurl=https%3A%2F%2Fvector.im%2F_matrix%2Fidentity%2Fapi%2Fv1%2Fsign-ed25519%3Ftoken%3Da_token" + + "&room_name=TestRoom" + + "&room_avatar_url=" + + "&inviter_name=User" + + "&guest_access_token=" + + "&guest_user_id=" + + "&room_type=" + assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) + } + + @Test + fun `parsing a url with signurl returns a room email invite link`() { + val url = "https://app.element.io/#/room/%21aBCDEF12345%3Amatrix.org" + + "?email=testuser%40element.io" + + "&signurl=https%3A%2F%2Fvector.im%2F_matrix%2Fidentity%2Fapi%2Fv1%2Fsign-ed25519%3Ftoken%3Da_token%26private_key%3Da_private_key" + + "&room_name=TestRoom" + + "&room_avatar_url=" + + "&inviter_name=User" + + "&guest_access_token=" + + "&guest_user_id=" + + "&room_type=" + assertThat(PermalinkParser.parse(url)).isEqualTo( + PermalinkData.RoomEmailInviteLink( + roomId = "!aBCDEF12345:matrix.org", + email = "testuser@element.io", + signUrl = "https://vector.im/_matrix/identity/api/v1/sign-ed25519?token=a_token&private_key=a_private_key", + roomName = "TestRoom", + roomAvatarUrl = "", + inviterName = "User", + identityServer = "vector.im", + token = "a_token", + privateKey = "a_private_key", + roomType = "", + ) + ) + } +} 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 8fc71dc99e..8b857d8076 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 @@ -48,6 +48,7 @@ import io.element.android.libraries.matrix.impl.notificationsettings.RustNotific import io.element.android.libraries.matrix.impl.oidc.toRustAction import io.element.android.libraries.matrix.impl.pushers.RustPushersService import io.element.android.libraries.matrix.impl.room.RoomContentForwarder +import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber import io.element.android.libraries.matrix.impl.room.RustMatrixRoom import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService import io.element.android.libraries.matrix.impl.roomlist.roomOrNull @@ -114,6 +115,7 @@ class RustMatrixClient constructor( private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock) private val notificationSettingsService = RustNotificationSettingsService(notificationSettings, dispatchers) + private val roomSyncSubscriber = RoomSyncSubscriber(innerRoomListService, dispatchers) private val isLoggingOut = AtomicBoolean(false) @@ -124,7 +126,16 @@ class RustMatrixClient constructor( Timber.v("didReceiveAuthError -> do the cleanup") //TODO handle isSoftLogout parameter. appCoroutineScope.launch { - doLogout(doRequest = false) + val existingData = sessionStore.getSession(client.userId()) + if (existingData != null) { + // Set isTokenValid to false + val newData = client.session().toSessionData( + isTokenValid = false, + loginType = existingData.loginType, + ) + sessionStore.updateData(newData) + } + doLogout(doRequest = false, removeSession = false) } } else { Timber.v("didReceiveAuthError -> already cleaning up") @@ -134,7 +145,12 @@ class RustMatrixClient constructor( override fun didRefreshTokens() { Timber.w("didRefreshTokens()") appCoroutineScope.launch { - sessionStore.updateData(client.session().toSessionData()) + val existingData = sessionStore.getSession(client.userId()) ?: return@launch + val newData = client.session().toSessionData( + isTokenValid = existingData.isTokenValid, + loginType = existingData.loginType, + ) + sessionStore.updateData(newData) } } } @@ -185,6 +201,7 @@ class RustMatrixClient constructor( systemClock = clock, roomContentForwarder = roomContentForwarder, sessionData = sessionStore.getSession(sessionId.value)!!, + roomSyncSubscriber = roomSyncSubscriber ) } } @@ -292,7 +309,6 @@ class RustMatrixClient constructor( runCatching { client.removeAvatar() } } - override fun syncService(): SyncService = rustSyncService override fun sessionVerificationService(): SessionVerificationService = verificationService @@ -326,9 +342,9 @@ class RustMatrixClient constructor( baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false) } - override suspend fun logout(): String? = doLogout(doRequest = true) + override suspend fun logout(): String? = doLogout(doRequest = true, removeSession = true) - private suspend fun doLogout(doRequest: Boolean): String? { + private suspend fun doLogout(doRequest: Boolean, removeSession: Boolean): String? { var result: String? = null withContext(sessionDispatcher) { if (doRequest) { @@ -340,7 +356,9 @@ class RustMatrixClient constructor( } close() baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true) - sessionStore.removeSession(sessionId.value) + if (removeSession) { + sessionStore.removeSession(sessionId.value) + } } return result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt index 8f7f63e503..423fe925dc 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt @@ -20,18 +20,19 @@ import io.element.android.libraries.matrix.api.auth.AuthenticationException import org.matrix.rustcomponents.sdk.AuthenticationException as RustAuthenticationException fun Throwable.mapAuthenticationException(): AuthenticationException { + val message = this.message ?: "Unknown error" return when (this) { - is RustAuthenticationException.ClientMissing -> AuthenticationException.ClientMissing(this.message!!) - is RustAuthenticationException.Generic -> AuthenticationException.Generic(this.message!!) - is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(this.message!!) - is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(this.message!!) - is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(this.message!!) - is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message!!) - is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message!!) - is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message!!) - is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!) - is RustAuthenticationException.OidcCancelled -> AuthenticationException.OidcError("OidcCancelled", message!!) - is RustAuthenticationException.OidcCallbackUrlInvalid -> AuthenticationException.OidcError("OidcCallbackUrlInvalid", message!!) - else -> AuthenticationException.Generic(this.message ?: "Unknown error") + is RustAuthenticationException.ClientMissing -> AuthenticationException.ClientMissing(message) + is RustAuthenticationException.Generic -> AuthenticationException.Generic(message) + is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(message) + is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(message) + is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(message) + is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message) + is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message) + is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message) + is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message) + is RustAuthenticationException.OidcCancelled -> AuthenticationException.OidcError("OidcCancelled", message) + is RustAuthenticationException.OidcCallbackUrlInvalid -> AuthenticationException.OidcError("OidcCallbackUrlInvalid", message) + else -> AuthenticationException.Generic(message) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index f2acb0b1be..033a5f6073 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -30,6 +30,8 @@ import io.element.android.libraries.matrix.impl.RustMatrixClientFactory import io.element.android.libraries.matrix.impl.exception.mapClientException import io.element.android.libraries.matrix.impl.mapper.toSessionData import io.element.android.libraries.network.useragent.UserAgentProvider +import io.element.android.libraries.sessionstorage.api.LoggedInState +import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -62,7 +64,7 @@ class RustMatrixAuthenticationService @Inject constructor( ) private var currentHomeserver = MutableStateFlow(null) - override fun isLoggedIn(): Flow { + override fun loggedInStateFlow(): Flow { return sessionStore.isLoggedIn() } @@ -74,7 +76,11 @@ class RustMatrixAuthenticationService @Inject constructor( runCatching { val sessionData = sessionStore.getSession(sessionId.value) if (sessionData != null) { - rustMatrixClientFactory.create(sessionData) + if (sessionData.isTokenValid) { + rustMatrixClientFactory.create(sessionData) + } else { + error("Token is not valid") + } } else { error("No session to restore with id $sessionId") } @@ -102,7 +108,12 @@ class RustMatrixAuthenticationService @Inject constructor( withContext(coroutineDispatchers.io) { runCatching { val client = authService.login(username, password, "Element X Android", null) - val sessionData = client.use { it.session().toSessionData() } + val sessionData = client.use { + it.session().toSessionData( + isTokenValid = true, + loginType = LoginType.PASSWORD, + ) + } sessionStore.storeData(sessionData) SessionId(sessionData.userId) }.mapFailure { failure -> @@ -144,7 +155,12 @@ class RustMatrixAuthenticationService @Inject constructor( runCatching { val urlForOidcLogin = pendingOidcAuthenticationData ?: error("You need to call `getOidcUrl()` first") val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl) - val sessionData = client.use { it.session().toSessionData() } + val sessionData = client.use { + it.session().toSessionData( + isTokenValid = true, + loginType = LoginType.OIDC, + ) + } pendingOidcAuthenticationData?.close() pendingOidcAuthenticationData = null sessionStore.storeData(sessionData) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt index 825c6f4397..fe21a460c8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt @@ -16,11 +16,15 @@ package io.element.android.libraries.matrix.impl.mapper +import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionData import org.matrix.rustcomponents.sdk.Session import java.util.Date -internal fun Session.toSessionData() = SessionData( +internal fun Session.toSessionData( + isTokenValid: Boolean, + loginType: LoginType, +) = SessionData( userId = userId, deviceId = deviceId, accessToken = accessToken, @@ -29,4 +33,6 @@ internal fun Session.toSessionData() = SessionData( oidcData = oidcData, slidingSyncProxy = slidingSyncProxy, loginTimestamp = Date(), + isTokenValid = isTokenValid, + loginType = loginType, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt index b82716cc2d..3d444f9a63 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt @@ -94,7 +94,7 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon } MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction MessageLikeEventContent.Sticker -> NotificationContent.MessageLike.Sticker - is MessageLikeEventContent.Poll -> NotificationContent.MessageLike.Poll(question) + is MessageLikeEventContent.Poll -> NotificationContent.MessageLike.Poll(senderId, question) } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSyncSubscriber.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSyncSubscriber.kt new file mode 100644 index 0000000000..4286fbb1ec --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSyncSubscriber.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.room + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.timeline.item.event.EventType +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.RequiredState +import org.matrix.rustcomponents.sdk.RoomListService +import org.matrix.rustcomponents.sdk.RoomSubscription +import timber.log.Timber + +class RoomSyncSubscriber( + private val roomListService: RoomListService, + private val dispatchers: CoroutineDispatchers, +) { + + private val subscriptionCounts = HashMap() + private val mutex = Mutex() + + private val settings = RoomSubscription( + requiredState = listOf( + RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""), + RequiredState(key = EventType.STATE_ROOM_TOPIC, value = ""), + RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""), + RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""), + ), + timelineLimit = null + ) + + suspend fun subscribe(roomId: RoomId) = mutex.withLock { + withContext(dispatchers.io) { + try { + val currentSubscription = subscriptionCounts.getOrElse(roomId) { 0 } + if (currentSubscription == 0) { + Timber.d("Subscribing to room $roomId}") + roomListService.room(roomId.value).use { roomListItem -> + roomListItem.subscribe(settings) + } + } + subscriptionCounts[roomId] = currentSubscription + 1 + } catch (exception: Exception) { + Timber.e("Failed to subscribe to room $roomId") + } + } + } + + suspend fun unsubscribe(roomId: RoomId) = mutex.withLock { + withContext(dispatchers.io) { + try { + val currentSubscription = subscriptionCounts.getOrElse(roomId) { 0 } + when (currentSubscription) { + 0 -> return@withContext + 1 -> { + Timber.d("Unsubscribe from room $roomId") + roomListService.room(roomId.value).use { roomListItem -> + roomListItem.unsubscribe() + } + } + } + subscriptionCounts[roomId] = currentSubscription - 1 + } catch (exception: Exception) { + Timber.e("Failed to unsubscribe from room $roomId") + } + } + } +} 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 4c20dd4d0c..2fd9760b21 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,7 +40,6 @@ import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.api.timeline.MatrixTimeline -import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl import io.element.android.libraries.matrix.impl.media.map @@ -61,12 +60,10 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.EventTimelineItem -import org.matrix.rustcomponents.sdk.RequiredState import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation -import org.matrix.rustcomponents.sdk.RoomSubscription import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.messageEventContentFromHtml import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown @@ -84,6 +81,7 @@ class RustMatrixRoom( private val systemClock: SystemClock, private val roomContentForwarder: RoomContentForwarder, private val sessionData: SessionData, + private val roomSyncSubscriber: RoomSyncSubscriber, ) : MatrixRoom { override val roomId = RoomId(innerRoom.id()) @@ -118,28 +116,15 @@ class RustMatrixRoom( override val timeline: MatrixTimeline = _timeline - override fun subscribeToSync() { - val settings = RoomSubscription( - requiredState = listOf( - RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""), - RequiredState(key = EventType.STATE_ROOM_TOPIC, value = ""), - RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""), - RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""), - ), - timelineLimit = null - ) - roomListItem.subscribe(settings) - } + override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId) - override fun unsubscribeFromSync() { - roomListItem.unsubscribe() - } + override suspend fun unsubscribeFromSync() = roomSyncSubscriber.unsubscribe(roomId) override fun destroy() { roomCoroutineScope.cancel() innerRoom.destroy() roomListItem.destroy() - inReplyToEventTimelineItem?.destroy() + specialModeEventTimelineItem?.destroy() } override val name: String? @@ -253,7 +238,14 @@ class RustMatrixRoom( withContext(roomDispatcher) { if (originalEventId != null) { runCatching { - innerRoom.edit(messageEventContentFromParts(body, htmlBody), originalEventId.value) + val editedEvent = specialModeEventTimelineItem ?: innerRoom.getEventTimelineItemByEventId(originalEventId.value) + editedEvent.use { + innerRoom.edit( + newContent = messageEventContentFromParts(body, htmlBody), + editItem = it, + ) + } + specialModeEventTimelineItem = null } } else { runCatching { @@ -263,23 +255,23 @@ class RustMatrixRoom( } } - private var inReplyToEventTimelineItem: EventTimelineItem? = null + private var specialModeEventTimelineItem: EventTimelineItem? = null - override suspend fun enterReplyMode(eventId: EventId): Result = withContext(roomDispatcher) { + override suspend fun enterSpecialMode(eventId: EventId?): Result = withContext(roomDispatcher) { runCatching { - inReplyToEventTimelineItem?.destroy() - inReplyToEventTimelineItem = null - inReplyToEventTimelineItem = innerRoom.getEventTimelineItemByEventId(eventId.value) + specialModeEventTimelineItem?.destroy() + specialModeEventTimelineItem = null + specialModeEventTimelineItem = eventId?.let { innerRoom.getEventTimelineItemByEventId(it.value) } } } override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result = withContext(roomDispatcher) { runCatching { - val inReplyTo = inReplyToEventTimelineItem ?: innerRoom.getEventTimelineItemByEventId(eventId.value) + val inReplyTo = specialModeEventTimelineItem ?: innerRoom.getEventTimelineItemByEventId(eventId.value) inReplyTo.use { eventTimelineItem -> innerRoom.sendReply(messageEventContentFromParts(body, htmlBody), eventTimelineItem) } - inReplyToEventTimelineItem = null + specialModeEventTimelineItem = null } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt index 28297426c4..b95eb95333 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt @@ -23,14 +23,14 @@ import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.catch -import org.matrix.rustcomponents.sdk.RoomList import org.matrix.rustcomponents.sdk.RoomListEntriesListener import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListEntry +import org.matrix.rustcomponents.sdk.RoomListInterface import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListLoadingState import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener -import org.matrix.rustcomponents.sdk.RoomListService +import org.matrix.rustcomponents.sdk.RoomListServiceInterface import org.matrix.rustcomponents.sdk.RoomListServiceState import org.matrix.rustcomponents.sdk.RoomListServiceStateListener import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator @@ -40,7 +40,7 @@ import timber.log.Timber private const val SYNC_INDICATOR_DELAY_BEFORE_SHOWING = 1000u private const val SYNC_INDICATOR_DELAY_BEFORE_HIDING = 0u -fun RoomList.loadingStateFlow(): Flow = +fun RoomListInterface.loadingStateFlow(): Flow = mxCallbackFlow { val listener = object : RoomListLoadingStateListener { override fun onUpdate(state: RoomListLoadingState) { @@ -58,7 +58,7 @@ fun RoomList.loadingStateFlow(): Flow = Timber.d(it, "loadingStateFlow() failed") }.buffer(Channel.UNLIMITED) -fun RoomList.entriesFlow(onInitialList: suspend (List) -> Unit): Flow> = +fun RoomListInterface.entriesFlow(onInitialList: suspend (List) -> Unit): Flow> = mxCallbackFlow { val listener = object : RoomListEntriesListener { override fun onUpdate(roomEntriesUpdate: List) { @@ -76,7 +76,7 @@ fun RoomList.entriesFlow(onInitialList: suspend (List) -> Unit): Timber.d(it, "entriesFlow() failed") }.buffer(Channel.UNLIMITED) -fun RoomListService.stateFlow(): Flow = +fun RoomListServiceInterface.stateFlow(): Flow = mxCallbackFlow { val listener = object : RoomListServiceStateListener { override fun onUpdate(state: RoomListServiceState) { @@ -88,7 +88,7 @@ fun RoomListService.stateFlow(): Flow = } }.buffer(Channel.UNLIMITED) -fun RoomListService.syncIndicator(): Flow = +fun RoomListServiceInterface.syncIndicator(): Flow = mxCallbackFlow { val listener = object : RoomListServiceSyncIndicatorListener { override fun onUpdate(syncIndicator: RoomListServiceSyncIndicator) { @@ -104,7 +104,7 @@ fun RoomListService.syncIndicator(): Flow = } }.buffer(Channel.UNLIMITED) -fun RoomListService.roomOrNull(roomId: String): RoomListItem? { +fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? { return try { room(roomId) } catch (exception: Exception) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt index 778637bdc7..754e35ec66 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt @@ -26,14 +26,14 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListEntry -import org.matrix.rustcomponents.sdk.RoomListService +import org.matrix.rustcomponents.sdk.RoomListServiceInterface import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.util.UUID class RoomSummaryListProcessor( private val roomSummaries: MutableStateFlow>, - private val roomListService: RoomListService, + private val roomListService: RoomListServiceInterface, private val dispatcher: CoroutineDispatcher, private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(), ) { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTests.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTests.kt new file mode 100644 index 0000000000..2a8dd5c122 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTests.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.auth + +import com.google.common.truth.ThrowableSubject +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.auth.AuthenticationException +import org.junit.Test +import org.matrix.rustcomponents.sdk.AuthenticationException as RustAuthenticationException + +class AuthenticationExceptionMappingTests { + + @Test + fun `mapping an exception with no message returns 'Unknown error' message`() { + val exception = Exception() + val mappedException = exception.mapAuthenticationException() + assertThat(mappedException.message).isEqualTo("Unknown error") + } + + @Test + fun `mapping a generic exception returns a Generic AuthenticationException`() { + val exception = Exception("Generic exception") + val mappedException = exception.mapAuthenticationException() + assertThat(mappedException).isException("Generic exception") + } + + @Test + fun `mapping specific exceptions map to their kotlin counterparts`() { + assertThat(RustAuthenticationException.ClientMissing("Client missing").mapAuthenticationException()) + .isException("Client missing") + + assertThat(RustAuthenticationException.Generic("Generic").mapAuthenticationException()).isException("Generic") + + assertThat(RustAuthenticationException.InvalidServerName("Invalid server name").mapAuthenticationException()) + .isException("Invalid server name") + + assertThat(RustAuthenticationException.SessionMissing("Session missing").mapAuthenticationException()) + .isException("Session missing") + + assertThat(RustAuthenticationException.SlidingSyncNotAvailable("Sliding sync not available").mapAuthenticationException()) + .isException("Sliding sync not available") + } + + @Test + fun `mapping Oidc related exceptions creates an 'OidcError' with different types`() { + assertIsOidcError( + throwable = RustAuthenticationException.OidcException("Oidc exception"), + type = "OidcException", + message = "Oidc exception" + ) + assertIsOidcError( + throwable = RustAuthenticationException.OidcMetadataInvalid("Oidc metadata invalid"), + type = "OidcMetadataInvalid", + message = "Oidc metadata invalid" + ) + assertIsOidcError( + throwable = RustAuthenticationException.OidcMetadataMissing("Oidc metadata missing"), + type = "OidcMetadataMissing", + message = "Oidc metadata missing" + ) + assertIsOidcError( + throwable = RustAuthenticationException.OidcNotSupported("Oidc not supported"), + type = "OidcNotSupported", + message = "Oidc not supported" + ) + assertIsOidcError( + throwable = RustAuthenticationException.OidcCancelled("Oidc cancelled"), + type = "OidcCancelled", + message = "Oidc cancelled" + ) + assertIsOidcError( + throwable = RustAuthenticationException.OidcCallbackUrlInvalid("Oidc callback url invalid"), + type = "OidcCallbackUrlInvalid", + message = "Oidc callback url invalid" + ) + } + + private inline fun ThrowableSubject.isException(message: String) { + isInstanceOf(T::class.java) + hasMessageThat().isEqualTo(message) + } + + private fun assertIsOidcError(throwable: Throwable, type: String, message: String) { + val authenticationException = throwable.mapAuthenticationException() + assertThat(authenticationException).isInstanceOf(AuthenticationException.OidcError::class.java) + assertThat((authenticationException as? AuthenticationException.OidcError)?.type).isEqualTo(type) + assertThat(authenticationException.message).isEqualTo(message) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt new file mode 100644 index 0000000000..5a376ea928 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.roomlist + +import com.google.common.truth.Truth.assertThat +import com.sun.jna.Pointer +import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID_2 +import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout +import org.junit.Test +import org.matrix.rustcomponents.sdk.RoomList +import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate +import org.matrix.rustcomponents.sdk.RoomListEntry +import org.matrix.rustcomponents.sdk.RoomListInput +import org.matrix.rustcomponents.sdk.RoomListItem +import org.matrix.rustcomponents.sdk.RoomListServiceInterface +import org.matrix.rustcomponents.sdk.RoomListServiceStateListener +import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener +import org.matrix.rustcomponents.sdk.TaskHandle +import kotlin.time.Duration.Companion.milliseconds + +// NOTE: this class is using a fake implementation of a Rust SDK interface which returns actual Rust objects with pointers. +// Since we don't access the data in those objects, this is fine for our tests, but that's as far as we can test this class. +class RoomSummaryListProcessorTests { + + private val summaries = MutableStateFlow>(emptyList()) + + @Test + fun `postUpdates can't start until postEntries is done`() = runTest { + val processor = createProcessor() + val update = listOf(RoomListEntriesUpdate.Reset(emptyList())) + + val timeoutError = runCatching { + withTimeout(10.milliseconds) { processor.postUpdate(update) } + }.exceptionOrNull() + assertThat(timeoutError).isInstanceOf(CancellationException::class.java) + + processor.postEntries(listOf(RoomListEntry.Empty)) + processor.postUpdate(update) + } + + @Test + fun `postEntries adds all new entries with no diffing`() = runTest { + summaries.value = listOf(aRoomSummaryFilled()) + val processor = createProcessor() + + processor.postEntries(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty)) + + assertThat(summaries.value.count()).isEqualTo(4) + } + + @Test + fun `Append adds new entries at the end of the list`() = runTest { + summaries.value = listOf(aRoomSummaryFilled()) + val processor = createProcessor() + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty)))) + + assertThat(summaries.value.count()).isEqualTo(4) + assertThat(summaries.value.subList(1, 4).all { it is RoomSummary.Empty }).isTrue() + } + + @Test + fun `PushBack adds a new entry at the end of the list`() = runTest { + summaries.value = listOf(aRoomSummaryFilled()) + val processor = createProcessor() + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(RoomListEntry.Empty))) + + assertThat(summaries.value.count()).isEqualTo(2) + assertThat(summaries.value.last()).isInstanceOf(RoomSummary.Empty::class.java) + } + + @Test + fun `PushFront inserts a new entry at the start of the list`() = runTest { + summaries.value = listOf(aRoomSummaryFilled()) + val processor = createProcessor() + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(RoomListEntry.Empty))) + + assertThat(summaries.value.count()).isEqualTo(2) + assertThat(summaries.value.first()).isInstanceOf(RoomSummary.Empty::class.java) + } + + @Test + fun `Set replaces an entry at some index`() = runTest { + summaries.value = listOf(aRoomSummaryFilled()) + val processor = createProcessor() + val index = 0 + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), RoomListEntry.Empty))) + + assertThat(summaries.value.count()).isEqualTo(1) + assertThat(summaries.value[index]).isInstanceOf(RoomSummary.Empty::class.java) + } + + @Test + fun `Insert inserts a new entry at the provided index`() = runTest { + summaries.value = listOf(aRoomSummaryFilled()) + val processor = createProcessor() + val index = 0 + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), RoomListEntry.Empty))) + + assertThat(summaries.value.count()).isEqualTo(2) + assertThat(summaries.value[index]).isInstanceOf(RoomSummary.Empty::class.java) + } + + @Test + fun `Remove removes an entry at some index`() = runTest { + summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) + val processor = createProcessor() + val index = 0 + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.Remove(index.toUInt()))) + + assertThat(summaries.value.count()).isEqualTo(1) + assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID_2.value) + } + + @Test + fun `PopBack removes an entry at the end of the list`() = runTest { + summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) + val processor = createProcessor() + val index = 0 + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.PopBack)) + + assertThat(summaries.value.count()).isEqualTo(1) + assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID.value) + } + + @Test + fun `PopFront removes an entry at the start of the list`() = runTest { + summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) + val processor = createProcessor() + val index = 0 + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.PopFront)) + + assertThat(summaries.value.count()).isEqualTo(1) + assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID_2.value) + } + + @Test + fun `Clear removes all the entries`() = runTest { + summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) + val processor = createProcessor() + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.Clear)) + + assertThat(summaries.value).isEmpty() + } + + @Test + fun `Truncate removes all entries after the provided length`() = runTest { + summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) + val processor = createProcessor() + val index = 0 + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.Truncate(1u))) + + assertThat(summaries.value.count()).isEqualTo(1) + assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID.value) + } + + private fun TestScope.createProcessor() = RoomSummaryListProcessor( + summaries, + fakeRoomListService, + dispatcher = StandardTestDispatcher(testScheduler), + roomSummaryDetailsFactory = RoomSummaryDetailsFactory(), + ) + + // Fake room list service that returns Rust objects with null pointers. Luckily for us, they don't crash for our test cases + private val fakeRoomListService = object : RoomListServiceInterface { + override suspend fun allRooms(): RoomList { + return RoomList(Pointer.NULL) + } + + override suspend fun applyInput(input: RoomListInput) = Unit + + override suspend fun invites(): RoomList { + return RoomList(Pointer.NULL) + } + + override fun room(roomId: String): RoomListItem { + return RoomListItem(Pointer.NULL) + } + + override fun state(listener: RoomListServiceStateListener): TaskHandle { + return TaskHandle(Pointer.NULL) + } + + override fun syncIndicator(delayBeforeShowingInMs: UInt, delayBeforeHidingInMs: UInt, listener: RoomListServiceSyncIndicatorListener): TaskHandle { + return TaskHandle(Pointer.NULL) + } + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt index 81fa3b677c..2cf6b77a78 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -36,9 +37,10 @@ class FakeAuthenticationService : MatrixAuthenticationService { private var oidcCancelError: Throwable? = null private var loginError: Throwable? = null private var changeServerError: Throwable? = null + private var matrixClient: MatrixClient? = null - override fun isLoggedIn(): Flow { - return flowOf(false) + override fun loggedInStateFlow(): Flow { + return flowOf(LoggedInState.NotLoggedIn) } override suspend fun getLatestSessionId(): SessionId? { @@ -46,7 +48,11 @@ class FakeAuthenticationService : MatrixAuthenticationService { } override suspend fun restoreSession(sessionId: SessionId): Result { - return Result.failure(IllegalStateException()) + return if (matrixClient != null) { + Result.success(matrixClient!!) + } else { + Result.failure(IllegalStateException()) + } } override fun getHomeserverDetails(): StateFlow { @@ -92,4 +98,8 @@ class FakeAuthenticationService : MatrixAuthenticationService { fun givenChangeServerError(throwable: Throwable?) { changeServerError = throwable } + + fun givenMatrixClient(matrixClient: MatrixClient) { + this.matrixClient = matrixClient + } } 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 e8abdb62df..4934867b55 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 @@ -157,9 +157,9 @@ class FakeMatrixRoom( override val timeline: MatrixTimeline = matrixTimeline - override fun subscribeToSync() = Unit + override suspend fun subscribeToSync() = Unit - override fun unsubscribeFromSync() = Unit + override suspend fun unsubscribeFromSync() = Unit override fun destroy() = Unit @@ -208,7 +208,7 @@ class FakeMatrixRoom( var replyMessageParameter: Pair? = null private set - override suspend fun enterReplyMode(eventId: EventId): Result { + override suspend fun enterSpecialMode(eventId: EventId?): Result { return Result.success(Unit) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableMatrixUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableMatrixUserRow.kt index 1e032de06a..d9bbff4f7d 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableMatrixUserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableMatrixUserRow.kt @@ -16,22 +16,13 @@ package io.element.android.libraries.matrix.ui.components -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.theme.components.Checkbox import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getBestName @@ -54,41 +45,6 @@ fun CheckableMatrixUserRow( enabled = enabled, ) -@Composable -fun CheckableUserRow( - checked: Boolean, - avatarData: AvatarData, - name: String, - subtext: String?, - modifier: Modifier = Modifier, - onCheckedChange: (Boolean) -> Unit = {}, - enabled: Boolean = true, -) { - Row( - modifier = modifier - .fillMaxWidth() - .clickable(role = Role.Checkbox, enabled = enabled) { - onCheckedChange(!checked) - }, - verticalAlignment = Alignment.CenterVertically, - ) { - UserRow( - modifier = Modifier.weight(1f), - avatarData = avatarData, - name = name, - subtext = subtext, - ) - - Checkbox( - modifier = Modifier - .padding(end = 16.dp), - checked = checked, - onCheckedChange = null, - enabled = enabled, - ) - } -} - @PreviewsDayNight @Composable internal fun CheckableMatrixUserRowPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableUnresolvedUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableUnresolvedUserRow.kt new file mode 100644 index 0000000000..b829ff8e47 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableUnresolvedUserRow.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.preview.ElementThemedPreview +import io.element.android.libraries.designsystem.theme.components.Checkbox +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.matrix.ui.model.getAvatarData + +@Composable +fun CheckableUnresolvedUserRow( + checked: Boolean, + avatarData: AvatarData, + id: String, + modifier: Modifier = Modifier, + onCheckedChange: (Boolean) -> Unit = {}, + enabled: Boolean = true, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(role = Role.Checkbox, enabled = enabled) { + onCheckedChange(!checked) + }, + verticalAlignment = Alignment.CenterVertically, + ) { + UnresolvedUserRow( + modifier = Modifier.weight(1f), + avatarData = avatarData, + id = id, + ) + + Checkbox( + modifier = Modifier.padding(end = 16.dp), + checked = checked, + onCheckedChange = null, + enabled = enabled, + ) + } +} + +@Preview +@Composable +internal fun CheckableUnresolvedUserRowPreview() = ElementThemedPreview { + val matrixUser = aMatrixUser() + Column { + CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value) + HorizontalDivider() + CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value) + HorizontalDivider() + CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value, enabled = false) + HorizontalDivider() + CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value, enabled = false) + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableUserRow.kt new file mode 100644 index 0000000000..f6272b9fda --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableUserRow.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.theme.components.Checkbox + +@Composable +fun CheckableUserRow( + checked: Boolean, + avatarData: AvatarData, + name: String, + subtext: String?, + modifier: Modifier = Modifier, + onCheckedChange: (Boolean) -> Unit = {}, + enabled: Boolean = true, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(role = Role.Checkbox, enabled = enabled) { + onCheckedChange(!checked) + }, + verticalAlignment = Alignment.CenterVertically, + ) { + UserRow( + modifier = Modifier.weight(1f), + avatarData = avatarData, + name = name, + subtext = subtext, + ) + + Checkbox( + modifier = Modifier + .padding(end = 16.dp), + checked = checked, + onCheckedChange = null, + enabled = enabled, + ) + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt index 44144deeb6..1380d15e8b 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt @@ -16,28 +16,15 @@ package io.element.android.libraries.matrix.ui.components -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.components.avatar.Avatar -import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getBestName -import io.element.android.libraries.theme.ElementTheme @Composable fun MatrixUserRow( @@ -51,47 +38,6 @@ fun MatrixUserRow( modifier = modifier, ) -@Composable -fun UserRow( - avatarData: AvatarData, - name: String, - subtext: String?, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .fillMaxWidth() - .heightIn(min = 56.dp) - .padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Avatar(avatarData) - Column( - modifier = Modifier - .padding(start = 12.dp), - ) { - // Name - Text( - text = name, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.primary, - style = ElementTheme.typography.fontBodyLgRegular, - ) - // Id - subtext?.let { - Text( - text = subtext, - color = MaterialTheme.colorScheme.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = ElementTheme.typography.fontBodySmRegular, - ) - } - } - } -} - @PreviewsDayNight @Composable internal fun MatrixUserRowPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt index 20c0ac69bc..2916fa7f06 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.matrix.ui.components -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -29,7 +28,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -38,8 +36,6 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.ElementThemedPreview -import io.element.android.libraries.designsystem.theme.components.Checkbox -import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.CommonDrawables @@ -101,58 +97,9 @@ fun UnresolvedUserRow( } } +@Preview @Composable -fun CheckableUnresolvedUserRow( - checked: Boolean, - avatarData: AvatarData, - id: String, - modifier: Modifier = Modifier, - onCheckedChange: (Boolean) -> Unit = {}, - enabled: Boolean = true, -) { - Row( - modifier = modifier - .fillMaxWidth() - .clickable(role = Role.Checkbox, enabled = enabled) { - onCheckedChange(!checked) - }, - verticalAlignment = Alignment.CenterVertically, - ) { - UnresolvedUserRow( - modifier = Modifier.weight(1f), - avatarData = avatarData, - id = id, - ) - - Checkbox( - modifier = Modifier.padding(end = 16.dp), - checked = checked, - onCheckedChange = null, - enabled = enabled, - ) - } +internal fun UnresolvedUserRowPreview() = ElementThemedPreview { + val matrixUser = aMatrixUser() + UnresolvedUserRow(matrixUser.getAvatarData(size = AvatarSize.UserListItem), matrixUser.userId.value) } - -@Preview -@Composable -internal fun UnresolvedUserRowPreview() = - ElementThemedPreview { - val matrixUser = aMatrixUser() - UnresolvedUserRow(matrixUser.getAvatarData(size = AvatarSize.UserListItem), matrixUser.userId.value) - } - -@Preview -@Composable -internal fun CheckableUnresolvedUserRowPreview() = - ElementThemedPreview { - val matrixUser = aMatrixUser() - Column { - CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value) - HorizontalDivider() - CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value) - HorizontalDivider() - CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value, enabled = false) - HorizontalDivider() - CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value, enabled = false) - } - } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt new file mode 100644 index 0000000000..5e59ce7aef --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.theme.ElementTheme + +@Composable +internal fun UserRow( + avatarData: AvatarData, + name: String, + subtext: String?, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .heightIn(min = 56.dp) + .padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Avatar(avatarData) + Column( + modifier = Modifier + .padding(start = 12.dp), + ) { + // Name + Text( + text = name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.primary, + style = ElementTheme.typography.fontBodyLgRegular, + ) + // Id + subtext?.let { + Text( + text = subtext, + color = MaterialTheme.colorScheme.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = ElementTheme.typography.fontBodySmRegular, + ) + } + } + } +} diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index 9fc160252b..cd968530f8 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -66,6 +66,8 @@ class AndroidMediaPreProcessor @Inject constructor( * values may surpass this limit. (i.e.: an image of `480x3000px` would have `inSampleSize=1` and be sent as is). */ private const val IMAGE_SCALE_REF_SIZE = 640 + + private val notCompressibleImageTypes = listOf(MimeTypes.Gif, MimeTypes.WebP) } private val contentResolver = context.contentResolver @@ -78,7 +80,10 @@ class AndroidMediaPreProcessor @Inject constructor( ): Result = withContext(coroutineDispatchers.computation) { runCatching { val result = when { - mimeType.isMimeTypeImage() -> processImage(uri, mimeType, compressIfPossible && mimeType != MimeTypes.Gif) + mimeType.isMimeTypeImage() -> { + val shouldBeCompressed = compressIfPossible && mimeType !in notCompressibleImageTypes + processImage(uri, mimeType, shouldBeCompressed) + } mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType, compressIfPossible) mimeType.isMimeTypeAudio() -> processAudio(uri, mimeType) else -> processFile(uri, mimeType) @@ -125,13 +130,11 @@ class AndroidMediaPreProcessor @Inject constructor( exifInterface?.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) } ?: ExifInterface.ORIENTATION_UNDEFINED - val compressionResult = contentResolver.openInputStream(uri).use { input -> - imageCompressor.compressToTmpFile( - inputStream = requireNotNull(input), - resizeMode = ResizeMode.Approximate(IMAGE_SCALE_REF_SIZE, IMAGE_SCALE_REF_SIZE), - orientation = orientation, - ).getOrThrow() - } + val compressionResult = imageCompressor.compressToTmpFile( + inputStreamProvider = { contentResolver.openInputStream(uri)!! }, + resizeMode = ResizeMode.Approximate(IMAGE_SCALE_REF_SIZE, IMAGE_SCALE_REF_SIZE), + orientation = orientation, + ).getOrThrow() val thumbnailResult: ThumbnailResult = thumbnailFactory.createImageThumbnail(compressionResult.file) val imageInfo = compressionResult.toImageInfo( mimeType = mimeType, diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt index a619a27bd9..d936b3a5bf 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt @@ -27,7 +27,6 @@ import io.element.android.libraries.androidutils.file.createTmpFile import io.element.android.libraries.di.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.io.BufferedInputStream import java.io.File import java.io.InputStream import javax.inject.Inject @@ -42,14 +41,14 @@ class ImageCompressor @Inject constructor( * @return a [Result] containing the resulting [ImageCompressionResult] with the temporary [File] and some metadata. */ suspend fun compressToTmpFile( - inputStream: InputStream, + inputStreamProvider: () -> InputStream, resizeMode: ResizeMode, format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, orientation: Int = ExifInterface.ORIENTATION_UNDEFINED, desiredQuality: Int = 80, ): Result = withContext(Dispatchers.IO) { runCatching { - val compressedBitmap = compressToBitmap(inputStream, resizeMode, orientation).getOrThrow() + val compressedBitmap = compressToBitmap(inputStreamProvider, resizeMode, orientation).getOrThrow() // Encode bitmap to the destination temporary file val tmpFile = context.createTmpFile(extension = "jpeg") tmpFile.outputStream().use { @@ -65,17 +64,24 @@ class ImageCompressor @Inject constructor( } /** - * Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode] and [orientation]. + * Decodes the inputStream from [inputStreamProvider] into a [Bitmap] and applies the needed transformations (rotation, scale) + * based on [resizeMode] and [orientation]. * @return a [Result] containing the resulting [Bitmap]. */ fun compressToBitmap( - inputStream: InputStream, + inputStreamProvider: () -> InputStream, resizeMode: ResizeMode, orientation: Int, ): Result = runCatching { - BufferedInputStream(inputStream).use { input -> - val options = BitmapFactory.Options() + val options = BitmapFactory.Options() + // Decode bounds + inputStreamProvider().use { input -> calculateDecodingScale(input, resizeMode, options) + } + // Decode the actual bitmap + inputStreamProvider().use { input -> + // Now read the actual image and rotate it to match its metadata + options.inJustDecodeBounds = false val decodedBitmap = BitmapFactory.decodeStream(input, null, options) ?: error("Decoding Bitmap from InputStream failed") val rotatedBitmap = decodedBitmap.rotateToMetadataOrientation(orientation) @@ -88,7 +94,7 @@ class ImageCompressor @Inject constructor( } private fun calculateDecodingScale( - inputStream: BufferedInputStream, + inputStream: InputStream, resizeMode: ResizeMode, options: BitmapFactory.Options ) { @@ -98,14 +104,10 @@ class ImageCompressor @Inject constructor( is ResizeMode.None -> return } // Read bounds only - inputStream.mark(inputStream.available()) options.inJustDecodeBounds = true BitmapFactory.decodeStream(inputStream, null, options) // Set sample size based on the outWidth and outHeight options.inSampleSize = options.calculateInSampleSize(width, height) - // Now read the actual image and rotate it to match its metadata - inputStream.reset() - options.inJustDecodeBounds = false } } diff --git a/libraries/permissions/api/src/main/res/values-de/translations.xml b/libraries/permissions/api/src/main/res/values-de/translations.xml index 5a16d89b4f..fb801cc450 100644 --- a/libraries/permissions/api/src/main/res/values-de/translations.xml +++ b/libraries/permissions/api/src/main/res/values-de/translations.xml @@ -1,7 +1,7 @@ - "Damit die Anwendung die Kamera verwenden kann, erteilen Sie bitte die Erlaubnis in den Systemeinstellungen." - "Bitte erteilen Sie die Erlaubnis in den Systemeinstellungen." - "Damit die Anwendung das Mikrofon verwenden kann, erteilen Sie bitte die Erlaubnis in den Systemeinstellungen." - "Damit die Anwendung Benachrichtigungen anzeigen kann, erteilen Sie bitte die Erlaubnis in den Systemeinstellungen." + "Damit die Anwendung die Kamera verwenden kann, erteile bitte die Erlaubnis in den Systemeinstellungen." + "Bitte erteile die Erlaubnis in den Systemeinstellungen." + "Damit die Anwendung das Mikrofon verwenden kann, erteile bitte die Erlaubnis in den Systemeinstellungen." + "Damit die Anwendung Benachrichtigungen anzeigen kann, erteile bitte die Erlaubnis in den Systemeinstellungen." diff --git a/libraries/permissions/api/src/main/res/values-fr/translations.xml b/libraries/permissions/api/src/main/res/values-fr/translations.xml index e0fac38135..1ddcac97a5 100644 --- a/libraries/permissions/api/src/main/res/values-fr/translations.xml +++ b/libraries/permissions/api/src/main/res/values-fr/translations.xml @@ -2,6 +2,6 @@ "Pour permettre à l’application d’utiliser l’appareil photo, veuillez accorder l’autorisation dans les paramètres du système." "Veuillez accorder l’autorisation dans les paramètres du système." - "Pour permettre à l\'application d’utiliser le microphone, veuillez accorder l’autorisation dans les paramètres du système." + "Pour permettre à l’application d’utiliser le microphone, veuillez accorder l’autorisation dans les paramètres du système." "Pour permettre à l’application d’afficher les notifications, veuillez accorder l’autorisation dans les paramètres du système." diff --git a/libraries/permissions/api/src/main/res/values-sk/translations.xml b/libraries/permissions/api/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..0670b59573 --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-sk/translations.xml @@ -0,0 +1,7 @@ + + + "Aby aplikácia mohla používať fotoaparát, udeľte povolenie v systémových nastaveniach." + "Udeľte prosím povolenie v systémových nastaveniach." + "Aby aplikácia mohla používať mikrofón, udeľte povolenie v systémových nastaveniach." + "Ak chcete, aby aplikácia zobrazovala oznámenia, udeľte povolenie v nastaveniach systému." + diff --git a/libraries/permissions/api/src/main/res/values-zh-rTW/translations.xml b/libraries/permissions/api/src/main/res/values-zh-rTW/translations.xml new file mode 100644 index 0000000000..cfd4762579 --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-zh-rTW/translations.xml @@ -0,0 +1,7 @@ + + + "為了讓應用程式使用相機,請到系統設定中開啟權限。" + "請到系統設定中開啟權限。" + "為了讓應用程式使用麥克風,請到系統設定中開啟權限。" + "為了讓應用程式顯示通知,請到系統設定中開啟權限。" + diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index 9951698b88..113a323611 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -87,7 +87,7 @@ class NotifiableEventResolver @Inject constructor( noisy = isNoisy, timestamp = this.timestamp, senderName = senderDisplayName, - body = descriptionFromMessageContent(content), + body = descriptionFromMessageContent(content, senderDisplayName ?: content.senderId.value), imageUriString = this.contentUrl, roomName = roomDisplayName, roomIsDirect = isDirect, @@ -133,9 +133,22 @@ class NotifiableEventResolver @Inject constructor( NotificationContent.MessageLike.KeyVerificationStart -> null.also { Timber.tag(loggerTag.value).d("Ignoring notification for verification ${content.javaClass.simpleName}") } - is NotificationContent.MessageLike.Poll -> null.also { - // TODO Polls: handle notification rendering - Timber.tag(loggerTag.value).d("Ignoring notification for poll") + is NotificationContent.MessageLike.Poll -> { + buildNotifiableMessageEvent( + sessionId = userId, + senderId = content.senderId, + roomId = roomId, + eventId = eventId, + noisy = isNoisy, + timestamp = this.timestamp, + senderName = senderDisplayName, + body = stringProvider.getString(CommonStrings.common_poll_summary, content.question), + imageUriString = null, + roomName = roomDisplayName, + roomIsDirect = isDirect, + roomAvatarPath = roomAvatarUrl, + senderAvatarPath = senderAvatarUrl, + ) } is NotificationContent.MessageLike.ReactionContent -> null.also { Timber.tag(loggerTag.value).d("Ignoring notification for reaction") @@ -192,10 +205,11 @@ class NotifiableEventResolver @Inject constructor( private fun descriptionFromMessageContent( content: NotificationContent.MessageLike.RoomMessage, + senderDisplayName: String, ): String { return when (val messageType = content.messageType) { is AudioMessageType -> messageType.body - is EmoteMessageType -> messageType.body + is EmoteMessageType -> "* $senderDisplayName ${messageType.body}" is FileMessageType -> messageType.body is ImageMessageType -> messageType.body is NoticeMessageType -> messageType.body diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index 6c9138fb15..cf5b13ce9b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -56,7 +56,7 @@ class DefaultPushHandler @Inject constructor( private val coroutineScope = CoroutineScope(SupervisorJob()) // UI handler - private val mUIHandler by lazy { + private val uiHandler by lazy { Handler(Looper.getMainLooper()) } @@ -81,7 +81,7 @@ class DefaultPushHandler @Inject constructor( return } - mUIHandler.post { + uiHandler.post { coroutineScope.launch(Dispatchers.IO) { handleInternal(pushData) } } } diff --git a/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml b/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml index 248fae8b0b..973981253b 100644 --- a/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml @@ -2,6 +2,7 @@ "通話" "無聲通知" + "** 無法傳送,請開啟聊天室" "加入" "拒絕" "邀請您聊天" @@ -10,6 +11,8 @@ "邀請您加入聊天室" "我" "您正在查看通知!點我!" + "%1$s:%2$s" + "%1$s:%2$s %3$s" "%1$s:%2$d 則訊息" diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoggedInState.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoggedInState.kt new file mode 100644 index 0000000000..ddf7ae5e8a --- /dev/null +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoggedInState.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.libraries.sessionstorage.api + +sealed interface LoggedInState { + data object NotLoggedIn : LoggedInState + data class LoggedIn( + val sessionId: String, + val isTokenValid: Boolean, + ) : LoggedInState +} diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoginType.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoginType.kt new file mode 100644 index 0000000000..ce29e3729a --- /dev/null +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/LoginType.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.sessionstorage.api + +// Imported from Element Android, to be able to migrate from EA to EXA. +enum class LoginType { + PASSWORD, + OIDC, + SSO, + UNSUPPORTED, + CUSTOM, + DIRECT, + UNKNOWN, + QR; + + companion object { + + fun fromName(name: String) = when (name) { + PASSWORD.name -> PASSWORD + OIDC.name -> OIDC + SSO.name -> SSO + UNSUPPORTED.name -> UNSUPPORTED + CUSTOM.name -> CUSTOM + DIRECT.name -> DIRECT + QR.name -> QR + else -> UNKNOWN + } + } +} diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt index e14c3feeab..25a48c0efe 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt @@ -27,4 +27,6 @@ data class SessionData( val oidcData: String?, val slidingSyncProxy: String?, val loginTimestamp: Date?, + val isTokenValid: Boolean, + val loginType: LoginType, ) diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt index 2b3398f76c..cc23353a8f 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map interface SessionStore { - fun isLoggedIn(): Flow + fun isLoggedIn(): Flow fun sessionsFlow(): Flow> suspend fun storeData(sessionData: SessionData) diff --git a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt index df78149eef..4b76e82e8b 100644 --- a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt +++ b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemorySessionStore.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.sessionstorage.impl.memory +import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.flow.Flow @@ -26,8 +27,17 @@ class InMemorySessionStore : SessionStore { private var sessionDataFlow = MutableStateFlow(null) - override fun isLoggedIn(): Flow { - return sessionDataFlow.map { it != null } + override fun isLoggedIn(): Flow { + return sessionDataFlow.map { + if (it == null) { + LoggedInState.NotLoggedIn + } else { + LoggedInState.LoggedIn( + sessionId = it.userId, + isTokenValid = it.isTokenValid, + ) + } + } } override fun sessionsFlow(): Flow> { diff --git a/libraries/session-storage/impl/build.gradle.kts b/libraries/session-storage/impl/build.gradle.kts index 698bfcf230..4e76c7de9c 100644 --- a/libraries/session-storage/impl/build.gradle.kts +++ b/libraries/session-storage/impl/build.gradle.kts @@ -45,10 +45,18 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(libs.coroutines.test) testImplementation(libs.sqldelight.driver.jvm) + + coreLibraryDesugaring(libs.android.desugar) } sqldelight { database("SessionDatabase") { + // https://cashapp.github.io/sqldelight/1.5.4/multiplatform_sqlite/migrations/ + // To generate a .db file from your latest schema, run this task + // ./gradlew generateDebugSessionDatabaseSchema + // Test migration by running + // ./gradlew verifySqlDelightMigration + schemaOutputDirectory = File("src/main/sqldelight/databases") verifyMigrations = true } } diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt index eb273411a0..0ca63d52a7 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt @@ -22,6 +22,7 @@ import com.squareup.sqldelight.runtime.coroutines.mapToList import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.flow.Flow @@ -35,11 +36,20 @@ class DatabaseSessionStore @Inject constructor( private val database: SessionDatabase, ) : SessionStore { - override fun isLoggedIn(): Flow { + override fun isLoggedIn(): Flow { return database.sessionDataQueries.selectFirst() .asFlow() .mapToOneOrNull() - .map { it != null } + .map { + if (it == null) { + LoggedInState.NotLoggedIn + } else { + LoggedInState.LoggedIn( + sessionId = it.userId, + isTokenValid = it.isTokenValid == 1L + ) + } + } } override suspend fun storeData(sessionData: SessionData) { diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt index d0c89d9896..1a81647f5c 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.sessionstorage.impl +import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionData import java.util.Date import io.element.android.libraries.matrix.session.SessionData as DbSessionData @@ -30,6 +31,8 @@ internal fun SessionData.toDbModel(): DbSessionData { oidcData = oidcData, slidingSyncProxy = slidingSyncProxy, loginTimestamp = loginTimestamp?.time, + isTokenValid = if (isTokenValid) 1L else 0L, + loginType = loginType.name, ) } @@ -42,6 +45,8 @@ internal fun DbSessionData.toApiModel(): SessionData { homeserverUrl = homeserverUrl, oidcData = oidcData, slidingSyncProxy = slidingSyncProxy, - loginTimestamp = loginTimestamp?.let { Date(it) } + loginTimestamp = loginTimestamp?.let { Date(it) }, + isTokenValid = isTokenValid == 1L, + loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name), ) } diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/3.db b/libraries/session-storage/impl/src/main/sqldelight/databases/3.db new file mode 100644 index 0000000000..58c8e8f09a Binary files /dev/null and b/libraries/session-storage/impl/src/main/sqldelight/databases/3.db differ diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/4.db b/libraries/session-storage/impl/src/main/sqldelight/databases/4.db new file mode 100644 index 0000000000..f2e7eb964f Binary files /dev/null and b/libraries/session-storage/impl/src/main/sqldelight/databases/4.db differ diff --git a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq index 05049c5635..8cebfbe5e2 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq +++ b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq @@ -6,7 +6,9 @@ CREATE TABLE SessionData ( homeserverUrl TEXT NOT NULL, slidingSyncProxy TEXT, loginTimestamp INTEGER, - oidcData TEXT + oidcData TEXT, + isTokenValid INTEGER NOT NULL, + loginType TEXT ); diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/3.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/3.sqm new file mode 100644 index 0000000000..c4d0743ff5 --- /dev/null +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/3.sqm @@ -0,0 +1,2 @@ +ALTER TABLE SessionData ADD COLUMN isTokenValid INTEGER NOT NULL DEFAULT 1; +ALTER TABLE SessionData ADD COLUMN loginType TEXT; diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt index e035ff9ae1..4ca932f48f 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt @@ -20,6 +20,8 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver import io.element.android.libraries.matrix.session.SessionData +import io.element.android.libraries.sessionstorage.api.LoggedInState +import io.element.android.libraries.sessionstorage.api.LoginType import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -38,6 +40,8 @@ class DatabaseSessionStoreTests { slidingSyncProxy = null, loginTimestamp = null, oidcData = "aOidcData", + isTokenValid = 1, + loginType = LoginType.UNKNOWN.name, ) @Before @@ -63,11 +67,11 @@ class DatabaseSessionStoreTests { @Test fun `isLoggedIn emits true while there are sessions in the DB`() = runTest { databaseSessionStore.isLoggedIn().test { - assertThat(awaitItem()).isFalse() + assertThat(awaitItem()).isEqualTo(LoggedInState.NotLoggedIn) database.sessionDataQueries.insertSessionData(aSessionData) - assertThat(awaitItem()).isTrue() + assertThat(awaitItem()).isEqualTo(LoggedInState.LoggedIn(sessionId = aSessionData.userId, isTokenValid = true)) database.sessionDataQueries.removeSession(aSessionData.userId) - assertThat(awaitItem()).isFalse() + assertThat(awaitItem()).isEqualTo(LoggedInState.NotLoggedIn) } } @@ -121,6 +125,8 @@ class DatabaseSessionStoreTests { slidingSyncProxy = "slidingSyncProxy", loginTimestamp = 1, oidcData = "aOidcData", + isTokenValid = 1, + loginType = null, ) val secondSessionData = SessionData( userId = "userId", @@ -131,6 +137,8 @@ class DatabaseSessionStoreTests { slidingSyncProxy = "slidingSyncProxyAltered", loginTimestamp = 2, oidcData = "aOidcDataAltered", + isTokenValid = 1, + loginType = null, ) assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId) assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp) diff --git a/libraries/textcomposer/impl/build.gradle.kts b/libraries/textcomposer/impl/build.gradle.kts index 86e911ca3e..1b96fec0e6 100644 --- a/libraries/textcomposer/impl/build.gradle.kts +++ b/libraries/textcomposer/impl/build.gradle.kts @@ -32,8 +32,6 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.testtags) - implementation(libs.androidx.constraintlayout) - implementation(libs.androidx.constraintlayout.compose) implementation(libs.matrix.richtexteditor) api(libs.matrix.richtexteditor.compose) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 4dac15d1b4..75b94cdfb9 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -16,54 +16,39 @@ package io.element.android.libraries.textcomposer -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension.Companion.fillToConstraints -import androidx.constraintlayout.compose.Visibility import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.text.applyScaleUp import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.matrix.api.core.EventId @@ -74,25 +59,26 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag -import io.element.android.libraries.textcomposer.components.FormattingOption -import io.element.android.libraries.textcomposer.components.FormattingOptionState +import io.element.android.libraries.textcomposer.components.ComposerOptionsButton +import io.element.android.libraries.textcomposer.components.DismissTextFormattingButton +import io.element.android.libraries.textcomposer.components.SendButton +import io.element.android.libraries.textcomposer.components.TextFormatting +import io.element.android.libraries.textcomposer.components.textInputRoundedCornerShape import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.wysiwyg.compose.RichTextEditor import io.element.android.wysiwyg.compose.RichTextEditorState -import io.element.android.wysiwyg.view.models.InlineFormat -import io.element.android.wysiwyg.view.models.LinkAction -import uniffi.wysiwyg_composer.ActionState -import uniffi.wysiwyg_composer.ComposerAction +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @Composable fun TextComposer( state: RichTextEditorState, composerMode: MessageComposerMode, - canSendMessage: Boolean, enableTextFormatting: Boolean, modifier: Modifier = Modifier, showTextFormatting: Boolean = false, + subcomposing: Boolean = false, onRequestFocus: () -> Unit = {}, onSendMessage: (Message) -> Unit = {}, onResetComposerMode: () -> Unit = {}, @@ -105,330 +91,214 @@ fun TextComposer( onSendMessage(Message(html = html, markdown = state.messageMarkdown)) } - Column( - modifier = modifier - .padding( - start = 3.dp, - end = 6.dp, - top = 8.dp, - bottom = 4.dp, - ) - .fillMaxWidth(), + val layoutModifier = modifier + .fillMaxSize() + .height(IntrinsicSize.Min) + + val composerOptionsButton = @Composable { + ComposerOptionsButton( + modifier = Modifier + .size(48.dp), + onClick = onAddAttachment + ) + } + + val textInput = @Composable { + TextInput( + state = state, + subcomposing = subcomposing, + placeholder = if (composerMode.inThread) { + stringResource(id = CommonStrings.action_reply_in_thread) + } else { + stringResource(id = R.string.rich_text_editor_composer_placeholder) + }, + composerMode = composerMode, + onResetComposerMode = onResetComposerMode, + onError = onError, + ) + } + + val sendButton = @Composable { + SendButton( + canSendMessage = state.messageHtml.isNotEmpty(), + onClick = onSendClicked, + composerMode = composerMode, + ) + } + + val textFormattingOptions = @Composable { TextFormatting(state = state) } + + if (showTextFormatting) { + TextFormattingLayout( + modifier = layoutModifier, + textInput = textInput, + dismissTextFormattingButton = { + DismissTextFormattingButton(onClick = onDismissTextFormatting) + }, + textFormatting = textFormattingOptions, + sendButton = sendButton + ) + } else { + StandardLayout( + modifier = layoutModifier, + composerOptionsButton = composerOptionsButton, + textInput = textInput, + sendButton = sendButton + ) + } + + if (!subcomposing) { + SoftKeyboardEffect(composerMode, onRequestFocus) { + it is MessageComposerMode.Special + } + + SoftKeyboardEffect(showTextFormatting, onRequestFocus) { it } + } +} + +@Composable +private fun StandardLayout( + textInput: @Composable () -> Unit, + composerOptionsButton: @Composable () -> Unit, + sendButton: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.Bottom, ) { - ConstraintLayout( - modifier = Modifier.fillMaxWidth(), + Box( + Modifier + .padding(bottom = 5.dp, top = 5.dp, start = 3.dp) ) { - val (composeOptions, textInput, sendButton) = createRefs() - val showComposerOptionsButton by remember(showTextFormatting) { - derivedStateOf { !showTextFormatting } - } - IconButton( - modifier = Modifier - .size(48.dp) - .constrainAs(composeOptions) { - start.linkTo(parent.start) - bottom.linkTo(parent.bottom) - visibility = if (showComposerOptionsButton) Visibility.Visible else Visibility.Gone - }, - onClick = onAddAttachment - ) { - Icon( - modifier = Modifier.size(30.dp.applyScaleUp()), - resourceId = CommonDrawables.ic_plus, - contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), - tint = ElementTheme.colors.iconPrimary, - ) - } - val roundCornerSmall = 20.dp.applyScaleUp() - val roundCornerLarge = 28.dp.applyScaleUp() - - val roundedCornerSize = remember(state.lineCount, composerMode) { - if (composerMode is MessageComposerMode.Special) { - roundCornerSmall - } else { - roundCornerLarge - } - } - val roundedCornerSizeState = animateDpAsState( - targetValue = roundedCornerSize, - animationSpec = tween( - durationMillis = 100, - ), - label = "roundedCornerSizeAnimation" - ) - val roundedCorners = RoundedCornerShape(roundedCornerSizeState.value) - val colors = ElementTheme.colors - val bgColor = colors.bgSubtleSecondary - val borderColor = colors.borderDisabled - - Column( - modifier = Modifier - .constrainAs(textInput) { - start.linkTo(composeOptions.end, margin = 3.dp, goneMargin = 9.dp) - end.linkTo(sendButton.start, margin = 6.dp, goneMargin = 6.dp) - bottom.linkTo(parent.bottom) - width = fillToConstraints - } - .padding(vertical = 3.dp) - .fillMaxWidth() - .clip(roundedCorners) - .background(color = bgColor) - .border(0.5.dp, borderColor, roundedCorners) - ) { - if (composerMode is MessageComposerMode.Special) { - ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode) - } - TextInput( - state = state, - placeholder = if (composerMode.inThread) { - stringResource(id = CommonStrings.action_reply_in_thread) - } else { - stringResource(id = R.string.rich_text_editor_composer_placeholder) - }, - roundedCorners = roundedCorners, - bgColor = bgColor, - onError = onError, - ) - } - - SendButton( - canSendMessage = canSendMessage, - onClick = onSendClicked, - composerMode = composerMode, - modifier = Modifier - .constrainAs(sendButton) { - bottom.linkTo(parent.bottom) - end.linkTo(parent.end) - visibility = if (!showTextFormatting) Visibility.Visible else Visibility.Gone - } - ) + composerOptionsButton() } - - if (showTextFormatting) { - TextFormatting( - state = state, - onDismiss = onDismissTextFormatting, - sendButton = { - SendButton( - canSendMessage = canSendMessage, - onClick = onSendClicked, - composerMode = composerMode, - modifier = it - ) - }, - ) + Box( + modifier = Modifier + .padding(bottom = 8.dp, top = 8.dp) + .weight(1f) + ) { + textInput() + } + Box( + Modifier + .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) + ) { + sendButton() } } +} - SoftKeyboardEffect(composerMode, onRequestFocus) { - it is MessageComposerMode.Special +@Composable +private fun TextFormattingLayout( + textInput: @Composable () -> Unit, + dismissTextFormattingButton: @Composable () -> Unit, + textFormatting: @Composable () -> Unit, + sendButton: @Composable () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.padding(vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Box( + modifier = Modifier + .weight(1f) + .padding(horizontal = 12.dp) + ) { + textInput() + } + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Box( + modifier = Modifier.padding(start = 3.dp) + ) { + dismissTextFormattingButton() + } + Box(modifier = Modifier.weight(1f)) { + textFormatting() + } + Box( + modifier = Modifier.padding( + start = 14.dp, + end = 6.dp + ) + ) { + sendButton() + } + } } - - SoftKeyboardEffect(showTextFormatting, onRequestFocus) { it } } @Composable private fun TextInput( state: RichTextEditorState, + subcomposing: Boolean, placeholder: String, - roundedCorners: RoundedCornerShape, - bgColor: Color, + composerMode: MessageComposerMode, + onResetComposerMode: () -> Unit, modifier: Modifier = Modifier, onError: (Throwable) -> Unit = {}, ) { - val minHeight = 42.dp.applyScaleUp() - val defaultTypography = ElementTheme.typography.fontBodyLgRegular - Box( + val bgColor = ElementTheme.colors.bgSubtleSecondary + val borderColor = ElementTheme.colors.borderDisabled + val roundedCorners = textInputRoundedCornerShape(composerMode = composerMode) + + Column( modifier = modifier - .heightIn(min = minHeight) - .background(color = bgColor, shape = roundedCorners) - .padding( - PaddingValues( + .clip(roundedCorners) + .border(0.5.dp, borderColor, roundedCorners) + .background(color = bgColor) + .requiredHeightIn(min = 42.dp.applyScaleUp()) + .fillMaxSize(), + ) { + if (composerMode is MessageComposerMode.Special) { + ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode) + } + val defaultTypography = ElementTheme.typography.fontBodyLgRegular + Box( + modifier = Modifier + .padding( top = 4.dp.applyScaleUp(), bottom = 4.dp.applyScaleUp(), start = 12.dp.applyScaleUp(), end = 42.dp.applyScaleUp() ) - ) - .testTag(TestTags.richTextEditor), - contentAlignment = Alignment.CenterStart, - ) { - - // Placeholder - if (state.messageHtml.isEmpty()) { - Text( - placeholder, - style = defaultTypography.copy( - color = ElementTheme.colors.textSecondary, - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - - RichTextEditor( - state = state, - modifier = Modifier - .padding(top = 6.dp, bottom = 6.dp) - .fillMaxWidth(), - style = ElementRichTextEditorStyle.create( - hasFocus = state.hasFocus - ), - onError = onError - ) - } -} - -@Composable -private fun TextFormatting( - state: RichTextEditorState, - onDismiss: () -> Unit, - modifier: Modifier = Modifier, - sendButton: @Composable (modifier: Modifier) -> Unit, -) { - ConstraintLayout( - modifier = modifier - .fillMaxWidth() - ) { - val (close, formatting, send) = createRefs() - - IconButton( - modifier = Modifier - .size(48.dp) - .constrainAs(close) { - start.linkTo(parent.start) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - }, - onClick = onDismiss + .testTag(TestTags.richTextEditor), + contentAlignment = Alignment.CenterStart, ) { - Icon( - modifier = Modifier.size(30.dp.applyScaleUp()), - resourceId = CommonDrawables.ic_cancel, - contentDescription = stringResource(CommonStrings.action_close), - tint = ElementTheme.colors.iconPrimary, - ) - } - - val scrollState = rememberScrollState() - Row( - modifier = Modifier - .constrainAs(formatting) { - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - start.linkTo(close.end, margin = 1.dp) - end.linkTo(send.start, margin = 14.dp) - width = fillToConstraints - } - .horizontalScroll(scrollState), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - FormattingOption( - state = state.actions[ComposerAction.BOLD].toButtonState(), - onClick = { state.toggleInlineFormat(InlineFormat.Bold) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_bold), - contentDescription = stringResource(R.string.rich_text_editor_format_bold) - ) - FormattingOption( - state = state.actions[ComposerAction.ITALIC].toButtonState(), - onClick = { state.toggleInlineFormat(InlineFormat.Italic) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_italic), - contentDescription = stringResource(R.string.rich_text_editor_format_italic) - ) - FormattingOption( - state = state.actions[ComposerAction.UNDERLINE].toButtonState(), - onClick = { state.toggleInlineFormat(InlineFormat.Underline) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_underline), - contentDescription = stringResource(R.string.rich_text_editor_format_underline) - ) - FormattingOption( - state = state.actions[ComposerAction.STRIKE_THROUGH].toButtonState(), - onClick = { state.toggleInlineFormat(InlineFormat.StrikeThrough) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_strikethrough), - contentDescription = stringResource(R.string.rich_text_editor_format_strikethrough) - ) - - var linkDialogAction by remember { mutableStateOf(null) } - - linkDialogAction?.let { - TextComposerLinkDialog( - onDismissRequest = { linkDialogAction = null }, - onCreateLinkRequest = state::insertLink, - onSaveLinkRequest = state::setLink, - onRemoveLinkRequest = state::removeLink, - linkAction = it, + // Placeholder + if (state.messageHtml.isEmpty()) { + Text( + placeholder, + style = defaultTypography.copy( + color = ElementTheme.colors.textSecondary, + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } - FormattingOption( - state = state.actions[ComposerAction.LINK].toButtonState(), - onClick = { linkDialogAction = state.linkAction }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_link), - contentDescription = stringResource(R.string.rich_text_editor_link) - ) - - FormattingOption( - state = state.actions[ComposerAction.UNORDERED_LIST].toButtonState(), - onClick = { state.toggleList(ordered = false) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_bullet_list), - contentDescription = stringResource(R.string.rich_text_editor_bullet_list) - ) - FormattingOption( - state = state.actions[ComposerAction.ORDERED_LIST].toButtonState(), - onClick = { state.toggleList(ordered = true) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_numbered_list), - contentDescription = stringResource(R.string.rich_text_editor_numbered_list) - ) - FormattingOption( - state = state.actions[ComposerAction.INDENT].toButtonState(), - onClick = { state.indent() }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_indent_increase), - contentDescription = stringResource(R.string.rich_text_editor_indent) - ) - FormattingOption( - state = state.actions[ComposerAction.UNINDENT].toButtonState(), - onClick = { state.unindent() }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_indent_decrease), - contentDescription = stringResource(R.string.rich_text_editor_unindent) - ) - FormattingOption( - state = state.actions[ComposerAction.INLINE_CODE].toButtonState(), - onClick = { state.toggleInlineFormat(InlineFormat.InlineCode) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_inline_code), - contentDescription = stringResource(R.string.rich_text_editor_inline_code) - ) - FormattingOption( - state = state.actions[ComposerAction.CODE_BLOCK].toButtonState(), - onClick = { state.toggleCodeBlock() }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_code_block), - contentDescription = stringResource(R.string.rich_text_editor_code_block) - ) - FormattingOption( - state = state.actions[ComposerAction.QUOTE].toButtonState(), - onClick = { state.toggleQuote() }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_quote), - contentDescription = stringResource(R.string.rich_text_editor_quote) + RichTextEditor( + state = state, + // Disable most of the editor functionality if it's just being measured for a subcomposition. + // This prevents it gaining focus and mutating the state. + registerStateUpdates = !subcomposing, + modifier = Modifier + .padding(top = 6.dp, bottom = 6.dp) + .fillMaxWidth(), + style = ElementRichTextEditorStyle.create( + hasFocus = state.hasFocus + ), + onError = onError ) } - - sendButton( - Modifier.constrainAs(send) { - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - end.linkTo(parent.end) - }, - ) } } -private fun ActionState?.toButtonState(): FormattingOptionState = - when (this) { - ActionState.ENABLED -> FormattingOptionState.Default - ActionState.REVERSED -> FormattingOptionState.Selected - ActionState.DISABLED, null -> FormattingOptionState.Disabled - } - @Composable private fun ComposerModeView( composerMode: MessageComposerMode, @@ -561,148 +431,96 @@ private fun ReplyToModeView( } } -@Composable -private fun SendButton( - canSendMessage: Boolean, - onClick: () -> Unit, - composerMode: MessageComposerMode, - modifier: Modifier = Modifier, -) { - IconButton( - modifier = modifier - .size(48.dp.applyScaleUp()), - onClick = onClick, - enabled = canSendMessage, - ) { - val iconId = when (composerMode) { - is MessageComposerMode.Edit -> CommonDrawables.ic_compound_check - else -> CommonDrawables.ic_september_send - } - val iconSize = when (composerMode) { - is MessageComposerMode.Edit -> 24.dp - // CommonDrawables.ic_september_send is too big... reduce its size. - else -> 18.dp - } - val iconStartPadding = when (composerMode) { - is MessageComposerMode.Edit -> 0.dp - else -> 2.dp - } - val contentDescription = when (composerMode) { - is MessageComposerMode.Edit -> stringResource(CommonStrings.action_edit) - else -> stringResource(CommonStrings.action_send) - } - Box( - modifier = Modifier - .clip(CircleShape) - .size(36.dp.applyScaleUp()) - .background(if (canSendMessage) ElementTheme.colors.iconAccentTertiary else Color.Transparent) - ) { - Icon( - modifier = Modifier - .height(iconSize.applyScaleUp()) - .padding(start = iconStartPadding) - .align(Alignment.Center), - resourceId = iconId, - contentDescription = contentDescription, - // Exception here, we use Color.White instead of ElementTheme.colors.iconOnSolidPrimary - tint = if (canSendMessage) Color.White else ElementTheme.colors.iconDisabled - ) - } - } -} - @PreviewsDayNight @Composable internal fun TextComposerSimplePreview() = ElementPreview { - Column { + PreviewColumn(items = persistentListOf( + { + TextComposer( + RichTextEditorState("", initialFocus = true), + onSendMessage = {}, + composerMode = MessageComposerMode.Normal(""), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + }, { TextComposer( - RichTextEditorState("", fake = true).apply { requestFocus() }, - canSendMessage = false, - onSendMessage = {}, - composerMode = MessageComposerMode.Normal(""), - onResetComposerMode = {}, - enableTextFormatting = true, - ) - TextComposer( - RichTextEditorState("A message", fake = true).apply { requestFocus() }, - canSendMessage = true, + RichTextEditorState("A message", initialFocus = true), onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, enableTextFormatting = true, ) + }, { TextComposer( RichTextEditorState( "A message\nWith several lines\nTo preview larger textfields and long lines with overflow", - fake = true - ).apply { - requestFocus() - }, - canSendMessage = true, + initialFocus = true + ), onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, enableTextFormatting = true, ) + }, { TextComposer( - RichTextEditorState("A message without focus", fake = true), - canSendMessage = true, + RichTextEditorState("A message without focus", initialFocus = false), onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, enableTextFormatting = true, ) - } -} - -@PreviewsDayNight -@Composable -internal fun TextComposerFormattingPreview() = ElementPreview { - Column { - TextComposer( - RichTextEditorState("", fake = true), - canSendMessage = false, - showTextFormatting = true, - composerMode = MessageComposerMode.Normal(""), - enableTextFormatting = true, - ) - TextComposer( - RichTextEditorState("A message", fake = true), - canSendMessage = true, - showTextFormatting = true, - composerMode = MessageComposerMode.Normal(""), - enableTextFormatting = true, - ) - TextComposer( - RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", fake = true), - canSendMessage = true, - showTextFormatting = true, - composerMode = MessageComposerMode.Normal(""), - enableTextFormatting = true, - ) - } -} - -@PreviewsDayNight -@Composable -internal fun TextComposerEditPreview() = ElementPreview { - TextComposer( - RichTextEditorState("A message", fake = true).apply { requestFocus() }, - canSendMessage = true, - onSendMessage = {}, - composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")), - onResetComposerMode = {}, - enableTextFormatting = true, + }) ) } @PreviewsDayNight @Composable -internal fun TextComposerReplyPreview() = ElementPreview { - Column { +internal fun TextComposerFormattingPreview() = ElementPreview { + PreviewColumn(items = persistentListOf({ TextComposer( - RichTextEditorState("", fake = true), - canSendMessage = false, + RichTextEditorState("", initialFocus = false), + showTextFormatting = true, + composerMode = MessageComposerMode.Normal(""), + enableTextFormatting = true, + ) + }, { + TextComposer( + RichTextEditorState("A message", initialFocus = false), + showTextFormatting = true, + composerMode = MessageComposerMode.Normal(""), + enableTextFormatting = true, + ) + }, { + TextComposer( + RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", initialFocus = false), + showTextFormatting = true, + composerMode = MessageComposerMode.Normal(""), + enableTextFormatting = true, + ) + })) +} + +@PreviewsDayNight +@Composable +internal fun TextComposerEditPreview() = ElementPreview { + PreviewColumn(items = persistentListOf({ + TextComposer( + RichTextEditorState("A message", initialFocus = true), + onSendMessage = {}, + composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + })) +} + +@PreviewsDayNight +@Composable +internal fun TextComposerReplyPreview() = ElementPreview { + PreviewColumn(items = persistentListOf({ + TextComposer( + RichTextEditorState(""), onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = false, @@ -716,25 +534,26 @@ internal fun TextComposerReplyPreview() = ElementPreview { onResetComposerMode = {}, enableTextFormatting = true, ) + }, + { + TextComposer( + RichTextEditorState(""), + onSendMessage = {}, + composerMode = MessageComposerMode.Reply( + isThreaded = true, + senderName = "Alice", + eventId = EventId("$1234"), + attachmentThumbnailInfo = null, + defaultContent = "A message\n" + + "With several lines\n" + + "To preview larger textfields and long lines with overflow" + ), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + }, { TextComposer( - RichTextEditorState("", fake = true), - canSendMessage = false, - onSendMessage = {}, - composerMode = MessageComposerMode.Reply( - isThreaded = true, - senderName = "Alice", - eventId = EventId("$1234"), - attachmentThumbnailInfo = null, - defaultContent = "A message\n" + - "With several lines\n" + - "To preview larger textfields and long lines with overflow" - ), - onResetComposerMode = {}, - enableTextFormatting = true, - ) - TextComposer( - RichTextEditorState("A message", fake = true), - canSendMessage = true, + RichTextEditorState("A message"), onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = true, @@ -751,9 +570,9 @@ internal fun TextComposerReplyPreview() = ElementPreview { onResetComposerMode = {}, enableTextFormatting = true, ) + }, { TextComposer( - RichTextEditorState("A message", fake = true), - canSendMessage = true, + RichTextEditorState("A message"), onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = false, @@ -770,9 +589,9 @@ internal fun TextComposerReplyPreview() = ElementPreview { onResetComposerMode = {}, enableTextFormatting = true, ) + }, { TextComposer( - RichTextEditorState("A message", fake = true), - canSendMessage = true, + RichTextEditorState("A message"), onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = false, @@ -789,9 +608,9 @@ internal fun TextComposerReplyPreview() = ElementPreview { onResetComposerMode = {}, enableTextFormatting = true, ) + }, { TextComposer( - RichTextEditorState("A message", fake = true).apply { requestFocus() }, - canSendMessage = true, + RichTextEditorState("A message", initialFocus = true), onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = false, @@ -808,5 +627,24 @@ internal fun TextComposerReplyPreview() = ElementPreview { onResetComposerMode = {}, enableTextFormatting = true, ) + }) + ) +} + +@Composable +private fun PreviewColumn( + items: ImmutableList<@Composable () -> Unit>, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + ) { + items.forEach { item -> + Box( + modifier = Modifier.height(IntrinsicSize.Min) + ) { + item() + } + } } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/ComposerOptionsButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/ComposerOptionsButton.kt new file mode 100644 index 0000000000..d1c7355861 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/ComposerOptionsButton.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.textcomposer.components + +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.applyScaleUp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.textcomposer.R +import io.element.android.libraries.theme.ElementTheme + +@Composable +internal fun ComposerOptionsButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier + .size(48.dp), + onClick = onClick + ) { + Icon( + modifier = Modifier.size(30.dp.applyScaleUp()), + resourceId = CommonDrawables.ic_plus, + contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), + tint = ElementTheme.colors.iconPrimary, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun ComposerOptionsButtonPreview() = ElementPreview { + ComposerOptionsButton(onClick = {}) +} + diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/DismissTextFormattingButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/DismissTextFormattingButton.kt new file mode 100644 index 0000000000..c6ebe270bf --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/DismissTextFormattingButton.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.textcomposer.components + +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.applyScaleUp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +internal fun DismissTextFormattingButton( + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + IconButton( + modifier = modifier + .size(48.dp), + onClick = onClick + ) { + Icon( + modifier = Modifier.size(30.dp.applyScaleUp()), + resourceId = CommonDrawables.ic_cancel, + contentDescription = stringResource(CommonStrings.action_close), + tint = ElementTheme.colors.iconPrimary, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun DismissTextFormattingButtonPreview() = ElementPreview { + DismissTextFormattingButton(onClick = {}) +} + diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt new file mode 100644 index 0000000000..5ac16bfd14 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.textcomposer.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.applyScaleUp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +internal fun SendButton( + canSendMessage: Boolean, + onClick: () -> Unit, + composerMode: MessageComposerMode, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier + .size(48.dp.applyScaleUp()), + onClick = onClick, + enabled = canSendMessage, + ) { + val iconId = when (composerMode) { + is MessageComposerMode.Edit -> CommonDrawables.ic_compound_check + else -> CommonDrawables.ic_september_send + } + val iconSize = when (composerMode) { + is MessageComposerMode.Edit -> 24.dp + // CommonDrawables.ic_september_send is too big... reduce its size. + else -> 18.dp + } + val iconStartPadding = when (composerMode) { + is MessageComposerMode.Edit -> 0.dp + else -> 2.dp + } + val contentDescription = when (composerMode) { + is MessageComposerMode.Edit -> stringResource(CommonStrings.action_edit) + else -> stringResource(CommonStrings.action_send) + } + Box( + modifier = Modifier + .clip(CircleShape) + .size(36.dp.applyScaleUp()) + .background(if (canSendMessage) ElementTheme.colors.iconAccentTertiary else Color.Transparent) + ) { + Icon( + modifier = Modifier + .height(iconSize.applyScaleUp()) + .padding(start = iconStartPadding) + .align(Alignment.Center), + resourceId = iconId, + contentDescription = contentDescription, + // Exception here, we use Color.White instead of ElementTheme.colors.iconOnSolidPrimary + tint = if (canSendMessage) Color.White else ElementTheme.colors.iconDisabled + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun SendButtonPreview() = ElementPreview { + val normalMode = MessageComposerMode.Normal("") + val editMode = MessageComposerMode.Edit(null, "", null) + Row { + SendButton(canSendMessage = true, onClick = {}, composerMode = normalMode) + SendButton(canSendMessage = false, onClick = {}, composerMode = normalMode) + SendButton(canSendMessage = true, onClick = {}, composerMode = editMode) + SendButton(canSendMessage = false, onClick = {}, composerMode = editMode) + } +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt new file mode 100644 index 0000000000..2df01c5f3a --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.textcomposer.components + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.textcomposer.R +import io.element.android.libraries.textcomposer.TextComposerLinkDialog +import io.element.android.wysiwyg.compose.RichTextEditorState +import io.element.android.wysiwyg.view.models.InlineFormat +import io.element.android.wysiwyg.view.models.LinkAction +import kotlinx.coroutines.launch +import uniffi.wysiwyg_composer.ActionState +import uniffi.wysiwyg_composer.ComposerAction +@Composable +internal fun TextFormatting( + state: RichTextEditorState, + modifier: Modifier = Modifier, +) { + + val scrollState = rememberScrollState() + val coroutineScope = rememberCoroutineScope() + + fun onInlineFormatClick(inlineFormat: InlineFormat) { + coroutineScope.launch { + state.toggleInlineFormat(inlineFormat) + } + } + + fun onToggleListClick(ordered: Boolean) { + coroutineScope.launch { + state.toggleList(ordered) + } + } + + fun onIndentClick() { + coroutineScope.launch { + state.indent() + } + } + + fun onUnindentClick() { + coroutineScope.launch { + state.unindent() + } + } + + fun onCodeBlockClick() { + coroutineScope.launch { + state.toggleCodeBlock() + } + } + + fun onQuoteClick() { + coroutineScope.launch { + state.toggleQuote() + } + } + + fun onCreateLinkRequest(url: String, text: String) { + coroutineScope.launch { + state.insertLink(url, text) + } + } + + fun onSaveLinkRequest(url: String) { + coroutineScope.launch { + state.setLink(url) + } + } + + fun onRemoveLinkRequest() { + coroutineScope.launch { + state.removeLink() + } + } + + Row( + modifier = modifier + .horizontalScroll(scrollState), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + FormattingOption( + state = state.actions[ComposerAction.BOLD].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.Bold) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_bold), + contentDescription = stringResource(R.string.rich_text_editor_format_bold) + ) + FormattingOption( + state = state.actions[ComposerAction.ITALIC].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.Italic) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_italic), + contentDescription = stringResource(R.string.rich_text_editor_format_italic) + ) + FormattingOption( + state = state.actions[ComposerAction.UNDERLINE].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.Underline) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_underline), + contentDescription = stringResource(R.string.rich_text_editor_format_underline) + ) + FormattingOption( + state = state.actions[ComposerAction.STRIKE_THROUGH].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.StrikeThrough) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_strikethrough), + contentDescription = stringResource(R.string.rich_text_editor_format_strikethrough) + ) + + var linkDialogAction by remember { mutableStateOf(null) } + + linkDialogAction?.let { + TextComposerLinkDialog( + onDismissRequest = { linkDialogAction = null }, + onCreateLinkRequest = ::onCreateLinkRequest, + onSaveLinkRequest = ::onSaveLinkRequest, + onRemoveLinkRequest = ::onRemoveLinkRequest, + linkAction = it, + ) + } + + FormattingOption( + state = state.actions[ComposerAction.LINK].toButtonState(), + onClick = { linkDialogAction = state.linkAction }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_link), + contentDescription = stringResource(R.string.rich_text_editor_link) + ) + + FormattingOption( + state = state.actions[ComposerAction.UNORDERED_LIST].toButtonState(), + onClick = { onToggleListClick(ordered = false) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_bullet_list), + contentDescription = stringResource(R.string.rich_text_editor_bullet_list) + ) + FormattingOption( + state = state.actions[ComposerAction.ORDERED_LIST].toButtonState(), + onClick = { onToggleListClick(ordered = true) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_numbered_list), + contentDescription = stringResource(R.string.rich_text_editor_numbered_list) + ) + FormattingOption( + state = state.actions[ComposerAction.INDENT].toButtonState(), + onClick = { onIndentClick() }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_indent_increase), + contentDescription = stringResource(R.string.rich_text_editor_indent) + ) + FormattingOption( + state = state.actions[ComposerAction.UNINDENT].toButtonState(), + onClick = { onUnindentClick() }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_indent_decrease), + contentDescription = stringResource(R.string.rich_text_editor_unindent) + ) + FormattingOption( + state = state.actions[ComposerAction.INLINE_CODE].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.InlineCode) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_inline_code), + contentDescription = stringResource(R.string.rich_text_editor_inline_code) + ) + FormattingOption( + state = state.actions[ComposerAction.CODE_BLOCK].toButtonState(), + onClick = { onCodeBlockClick() }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_code_block), + contentDescription = stringResource(R.string.rich_text_editor_code_block) + ) + FormattingOption( + state = state.actions[ComposerAction.QUOTE].toButtonState(), + onClick = { onQuoteClick() }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_quote), + contentDescription = stringResource(R.string.rich_text_editor_quote) + ) + } +} + +private fun ActionState?.toButtonState(): FormattingOptionState = + when (this) { + ActionState.ENABLED -> FormattingOptionState.Default + ActionState.REVERSED -> FormattingOptionState.Selected + ActionState.DISABLED, null -> FormattingOptionState.Disabled + } + +@PreviewsDayNight +@Composable +internal fun TextFormattingPreview() = ElementPreview { + TextFormatting(state = RichTextEditorState()) +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextInputRoundedCornerShape.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextInputRoundedCornerShape.kt new file mode 100644 index 0000000000..cf975634f6 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextInputRoundedCornerShape.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.textcomposer.components + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.text.applyScaleUp +import io.element.android.libraries.textcomposer.MessageComposerMode + +@Composable +internal fun textInputRoundedCornerShape( + composerMode: MessageComposerMode, +): RoundedCornerShape { + val roundCornerSmall = 20.dp.applyScaleUp() + val roundCornerLarge = 21.dp.applyScaleUp() + + val roundedCornerSize = if (composerMode is MessageComposerMode.Special) { + roundCornerSmall + } else { + roundCornerLarge + } + + val roundedCornerSizeState = animateDpAsState( + targetValue = roundedCornerSize, + animationSpec = tween( + durationMillis = 100, + ), + label = "roundedCornerSizeAnimation" + ) + return RoundedCornerShape(roundedCornerSizeState.value) +} diff --git a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ElementTheme.kt b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ElementTheme.kt index 81780d1c2d..3b87565271 100644 --- a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ElementTheme.kt +++ b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ElementTheme.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember @@ -138,27 +137,7 @@ fun ElementTheme( } } -/** - * Can be used to force a composable in dark theme. - * It will automatically change the system ui colors back to normal when leaving the composition. - */ -@Composable -fun ForcedDarkElementTheme( - lightStatusBar: Boolean = false, - content: @Composable () -> Unit, -) { - val systemUiController = rememberSystemUiController() - val colorScheme = MaterialTheme.colorScheme - val wasDarkTheme = !ElementTheme.colors.isLight - DisposableEffect(Unit) { - onDispose { - systemUiController.applyTheme(colorScheme, wasDarkTheme) - } - } - ElementTheme(darkTheme = true, lightStatusBar = lightStatusBar, content = content) -} - -private fun SystemUiController.applyTheme( +internal fun SystemUiController.applyTheme( colorScheme: ColorScheme, darkTheme: Boolean, ) { diff --git a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ForcedDarkElementTheme.kt b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ForcedDarkElementTheme.kt new file mode 100644 index 0000000000..c10ea74fc9 --- /dev/null +++ b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ForcedDarkElementTheme.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import com.google.accompanist.systemuicontroller.rememberSystemUiController + +/** + * Can be used to force a composable in dark theme. + * It will automatically change the system ui colors back to normal when leaving the composition. + */ +@Composable +fun ForcedDarkElementTheme( + lightStatusBar: Boolean = false, + content: @Composable () -> Unit, +) { + val systemUiController = rememberSystemUiController() + val colorScheme = MaterialTheme.colorScheme + val wasDarkTheme = !ElementTheme.colors.isLight + DisposableEffect(Unit) { + onDispose { + systemUiController.applyTheme(colorScheme, wasDarkTheme) + } + } + ElementTheme(darkTheme = true, lightStatusBar = lightStatusBar, content = content) +} diff --git a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/MaterialThemeColors.kt b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/MaterialThemeColors.kt index 3d359594e1..9df1db3505 100644 --- a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/MaterialThemeColors.kt +++ b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/MaterialThemeColors.kt @@ -91,7 +91,7 @@ internal val materialColorSchemeDark = darkColorScheme( @Preview @Composable -internal fun ColorsSchemePreviewLight() = ColorsSchemePreview( +internal fun ColorsSchemeLightPreview() = ColorsSchemePreview( Color.Black, Color.White, materialColorSchemeLight, @@ -99,7 +99,7 @@ internal fun ColorsSchemePreviewLight() = ColorsSchemePreview( @Preview @Composable -internal fun ColorsSchemePreviewDark() = ColorsSchemePreview( +internal fun ColorsSchemeDarkPreview() = ColorsSchemePreview( Color.White, Color.Black, materialColorSchemeDark, diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index 75a35d3190..a1e9f385f9 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -36,6 +36,7 @@ "Pozvat přátele do %1$s" "Pozvat lidi na %1$s" "Pozvánky" + "Vstoupit" "Zjistit více" "Odejít" "Opustit místnost" @@ -71,6 +72,7 @@ "Vyfotit" "Zobrazit zdroj" "Ano" + "Upravit hlasování" "O aplikaci" "Zásady používání" "Pokročilá nastavení" @@ -93,6 +95,7 @@ "GIF" "Obrázek" "V odpovědi na %1$s" + "Nainstalovat APK" "Tento Matrix identifikátor nelze najít, takže pozvánka nemusí být přijata." "Opuštění místnosti" "Odkaz zkopírován do schránky" @@ -122,6 +125,7 @@ "např. název vašeho projektu" "Hledat někoho" "Výsledky hledání" + "Zabezpečená záloha" "Zabezpečení" "Vyberte svůj server" "Odesílání…" @@ -148,7 +152,9 @@ "Ověření zrušeno" "Ověření dokončeno" "Video" + "Hlasová zpráva" "Čekání…" + "Opravdu chcete ukončit toto hlasování?" "Hlasování: %1$s" "Potvrzení" "Upozornění" diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index afdcc0e79c..a123d9a8bd 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -36,6 +36,7 @@ "Freunde einladen %1$s" "Lade Personen in %1$s ein" "Einladungen" + "Beitreten" "Mehr erfahren" "Verlassen" "Raum verlassen" @@ -71,6 +72,7 @@ "Foto machen" "Quelle anzeigen" "Ja" + "Umfrage bearbeiten" "Über" "Nutzungsrichtlinie" "Erweiterte Einstellungen" @@ -93,6 +95,7 @@ "GIF" "Bild" "Als Antwort auf %1$s" + "APK installieren" "Diese Matrix-ID kann nicht gefunden werden, daher wird die Einladung möglicherweise nicht empfangen." "Raum verlassen" "Link in die Zwischenablage kopiert" @@ -149,6 +152,7 @@ "Verifizierung abgeschlossen" "Video" "Warten…" + "Bist du sicher, dass du diese Umfrage beenden möchtest?" "Umfrage: %1$s" "Bestätigung" "Warnung" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 9e41ad281d..f05ab77402 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -36,6 +36,7 @@ "Inviter des amis à %1$s" "Invitez des personnes à %1$s" "Invitations" + "Rejoindre" "En savoir plus" "Quitter" "Quitter le salon" @@ -71,6 +72,7 @@ "Prendre une photo" "Afficher la source" "Oui" + "Modifier le sondage" "À propos" "Politique d’utilisation acceptable" "Paramètres avancés" @@ -93,6 +95,7 @@ "GIF" "Image" "En réponse à %1$s" + "Installer l’APK" "Cet identifiant Matrix est introuvable, il est donc possible que l’invitation ne soit pas reçue." "Quitter le salon…" "Lien copié dans le presse-papiers" @@ -149,6 +152,7 @@ "Vérification terminée" "Vidéo" "En attente…" + "Êtes-vous sûr de vouloir mettre fin à ce sondage ?" "Sondage : %1$s" "Confirmation" "Attention" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 3a9c44f975..4d536bc619 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -36,6 +36,7 @@ "Pozvať priateľov do %1$s" "Pozvať ľudí do %1$s" "Pozvánky" + "Pripojiť sa" "Zistiť viac" "Opustiť" "Opustiť miestnosť" @@ -45,6 +46,7 @@ "Nie" "Teraz nie" "OK" + "Otvoriť nastavenia" "Otvoriť pomocou" "Rýchla odpoveď" "Citovať" @@ -70,6 +72,7 @@ "Urobiť fotku" "Zobraziť zdroj" "Áno" + "Upraviť anketu" "O aplikácii" "Zásady prijateľného používania" "Pokročilé nastavenia" @@ -92,6 +95,7 @@ "GIF" "Obrázok" "V odpovedi na %1$s" + "Inštalovať APK" "Toto Matrix ID sa nedá nájsť, takže pozvánka nemusí byť prijatá." "Opustenie miestnosti" "Odkaz bol skopírovaný do schránky" @@ -106,6 +110,7 @@ "Heslo" "Ľudia" "Trvalý odkaz" + "Povolenie" "Celkový počet hlasov: %1$s" "Výsledky sa zobrazia po ukončení ankety" "Zásady ochrany osobných údajov" @@ -120,6 +125,7 @@ "napr. názov vášho projektu" "Vyhľadať niekoho" "Výsledky hľadania" + "Bezpečné zálohovanie" "Bezpečnosť" "Vyberte svoj server" "Odosiela sa…" @@ -146,7 +152,10 @@ "Overovanie zrušené" "Overovanie je dokončené" "Video" + "Hlasová správa" "Čaká sa…" + "Ste si istí, že chcete ukončiť túto anketu?" + "Anketa: %1$s" "Potvrdenie" "Upozornenie" "Aktivity" diff --git a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml index 2059c6f138..cc1b0102d1 100644 --- a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml @@ -9,6 +9,7 @@ "顯示密碼" "使用者選單" "接受" + "新增至時間軸" "返回" "取消" "選擇照片" @@ -30,9 +31,10 @@ "轉寄" "邀請" "邀請朋友" - "邀請朋友使用%1$s" - "邀請夥伴使用%1$s" + "邀請朋友使用 %1$s" + "邀請夥伴使用 %1$s" "邀請" + "加入" "了解更多" "離開" "離開聊天室" @@ -68,6 +70,7 @@ "拍照" "檢視原始碼" "是" + "編輯投票" "關於" "可接受使用政策" "進階設定" @@ -89,6 +92,7 @@ "GIF" "圖片" "回覆 %1$s" + "安裝 APK" "找不到此 Matrix ID,因此可能沒有人會收到邀請。" "正在離開聊天室" "連結已複製到剪貼簿" @@ -119,6 +123,7 @@ "安全性" "選擇您的伺服器" "傳送中…" + "伺服器不支援" "伺服器 URL" "設定" "貼圖" @@ -132,11 +137,14 @@ "無法發送邀請給一或多個使用者。" "無法發送邀請" "開啟通知" + "不支援的事件" "使用者名稱" "驗證已取消" "驗證完成" "影片" + "語音訊息" "等待中…" + "您確定要結束這項投票嗎?" "投票:%1$s" "確認" "警告" @@ -152,7 +160,10 @@ "%1$s無法載入地圖。請稍後再試。" "無法載入訊息" "%1$s無法取得您的位置。請稍後再試。" + "%1$s 沒有權限存取您的位置。您可以到設定中開啟權限。" + "%1$s 沒有權限存取您的位置。請在下方開啟權限。" "有些訊息尚未傳送" + "嘿,來 %1$s 和我聊天:%2$s" "您確定要離開聊天室嗎?這裡只有您一個人。如果您離開了,包含您在內的所有人都無法再進入此聊天室。" "您確定要離開聊天室嗎?此聊天室不是公開的,如果沒有收到邀請,您無法重新加入。" "您確定要離開聊天室嗎?" @@ -169,6 +180,8 @@ "無法上傳媒體檔案,請稍後再試。" "其他設定" "私訊" + "更新通知設定時發生錯誤。" + "所有訊息" "僅限提及與關鍵字" "在這個裝置上開啟通知" "群組聊天" @@ -190,7 +203,7 @@ "zh-tw" "錯誤" "成功" - "分享匿名的使用數據以協助我們釐清問題" + "分享匿名的使用數據以協助我們釐清問題。" "您可以到%1$s閱讀我們的條款。" "這裡" "封鎖使用者" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 18f5e63078..6a7f0ecbb7 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -36,6 +36,7 @@ "Invite friends to %1$s" "Invite people to %1$s" "Invites" + "Join" "Learn more" "Leave" "Leave room" @@ -54,7 +55,7 @@ "Reply" "Reply in thread" "Report bug" - "Report Content" + "Report content" "Retry" "Retry decryption" "Save" @@ -63,13 +64,14 @@ "Send message" "Share" "Share link" + "Sign in again" "Skip" "Start" "Start chat" "Start verification" "Tap to load map" "Take photo" - "View Source" + "View source" "Yes" "Edit poll" "About" @@ -124,6 +126,7 @@ "e.g. your project name" "Search for someone" "Search results" + "Secure backup" "Security" "Select your server" "Sending…" @@ -150,6 +153,7 @@ "Verification cancelled" "Verification complete" "Video" + "Voice message" "Waiting…" "Are you sure you want to end this poll?" "Poll: %1$s" diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index f34cdfbdc0..45e5b4adfd 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -56,7 +56,7 @@ private const val versionMinor = 2 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -private const val versionPatch = 3 +private const val versionPatch = 4 object Versions { val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt index ec4d7cf9f2..7eb24be57d 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt @@ -40,7 +40,7 @@ class LoginScreen(private val authenticationService: MatrixAuthenticationService } LaunchedEffect(Unit) { - authenticationService.setHomeserver(defaultAccountProvider.title) + authenticationService.setHomeserver(defaultAccountProvider.url) } val state = presenter.present() diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt index d0309ebec7..c95a44d8ea 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.impl.RustMatrixClientFactory import io.element.android.libraries.matrix.impl.auth.RustMatrixAuthenticationService import io.element.android.libraries.network.useragent.SimpleUserAgentProvider +import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore import io.element.android.libraries.theme.ElementTheme import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock @@ -64,8 +65,8 @@ class MainActivity : ComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) setContent { ElementTheme { - val isLoggedIn by matrixAuthenticationService.isLoggedIn().collectAsState(initial = false) - Content(isLoggedIn = isLoggedIn, modifier = Modifier.fillMaxSize()) + val loggedInState by matrixAuthenticationService.loggedInStateFlow().collectAsState(initial = LoggedInState.NotLoggedIn) + Content(isLoggedIn = loggedInState is LoggedInState.LoggedIn, modifier = Modifier.fillMaxSize()) } } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 425888f003..0c43619f39 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -31,7 +31,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.dateformatter.impl.DateFormatters import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.eventformatter.impl.DefaultRoomLastMessageFormatter import io.element.android.libraries.eventformatter.impl.ProfileChangeContentFormatter import io.element.android.libraries.eventformatter.impl.RoomMembershipContentFormatter 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 index cbc8f53e0b..80ac8d30fc 100644 --- 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 @@ -37,7 +37,7 @@ fun AppErrorView( } @Composable -fun AppErrorViewContent( +private fun AppErrorViewContent( title: String, body: String, onDismiss: () -> Unit = { }, diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/IsInDebug.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/IsInDebug.kt new file mode 100644 index 0000000000..d2bef1c4c2 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/IsInDebug.kt @@ -0,0 +1,22 @@ +/* + * 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.tests.testutils + +/** + * Returns true if the app is in debug mode. + */ +fun isInDebug() = BuildConfig.DEBUG diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomView-D-3_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomView-D-3_3_null_1,NEXUS_5,1.0,en].png index 768fd9e3d3..c872252b2e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomView-D-3_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomView-D-3_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9003614370c8dfcb82a9dbdfb1bd3244d22c0eae964e40944f247da849c57b73 -size 87620 +oid sha256:2f331af0d2dd34f4afef67a23e1b47d41bb31971fbe18752c61225a280f7f06f +size 84819 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomView-N-3_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomView-N-3_4_null_1,NEXUS_5,1.0,en].png index 16d242da00..03d4d04550 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomView-N-3_4_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.configureroom_null_ConfigureRoomView-N-3_4_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21f035c1a93bb79bf6fb6168a7c8df30833cabd847a1cfa948f0c3d0dff46a2e -size 84056 +oid sha256:57a2b92097bf41a2ef79c9f6f41ecca1d1b631fe15377458df2a5bd27165caa0 +size 81467 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.dialogs_null_SlidingSyncNotSupportedDialog-D-2_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.dialogs_null_SlidingSyncNotSupportedDialog-D-2_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7650c954d8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.dialogs_null_SlidingSyncNotSupportedDialog-D-2_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1883434cfce8e494f9ed92daf6ad885956b5d7f5c471968b443afe752b954aa7 +size 22827 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.dialogs_null_SlidingSyncNotSupportedDialog-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.dialogs_null_SlidingSyncNotSupportedDialog-N-2_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2d088f837d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.dialogs_null_SlidingSyncNotSupportedDialog-N-2_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:555f303cff58f52399fe6f8b8b6bc14cbea2cd71c084a66faa8bd2c0be2d0ff8 +size 19163 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-D-2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-D-3_3_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-D-2_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-D-3_3_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-D-3_3_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-D-2_2_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-D-3_3_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-N-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-N-3_4_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-N-2_3_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-N-3_4_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-N-3_4_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-N-2_3_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.oidc.webview_null_OidcView-N-3_4_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderView-D-3_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderView-D-4_4_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderView-D-3_3_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderView-D-4_4_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderView-N-3_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderView-N-4_5_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderView-N-3_4_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderView-N-4_5_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.confirmaccountprovider_null_ConfirmAccountProviderView-D-4_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.confirmaccountprovider_null_ConfirmAccountProviderView-D-5_5_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.confirmaccountprovider_null_ConfirmAccountProviderView-D-4_4_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.confirmaccountprovider_null_ConfirmAccountProviderView-D-5_5_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.confirmaccountprovider_null_ConfirmAccountProviderView-N-4_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.confirmaccountprovider_null_ConfirmAccountProviderView-N-5_6_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.confirmaccountprovider_null_ConfirmAccountProviderView-N-4_5_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.confirmaccountprovider_null_ConfirmAccountProviderView-N-5_6_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-5_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-5_5_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index dd0e30059c..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-5_5_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0a5ca3bd244941e910ae25e5d2d68f3aacb4ad7d89ed6439168c931d3b1f11df -size 39644 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-5_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-6_6_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-5_5_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-6_6_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-5_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-6_6_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-5_5_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-6_6_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-6_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-6_6_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..685f99c2d9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-D-6_6_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:544c75b9711b7baa0f63d1219c33281a143b3cb0ae601edcb2c070e7c4c57a0c +size 30747 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-5_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-5_6_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 0168b4f9a8..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-5_6_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c21cb3f0b3b594a8b3b63762ea7c0864ed6010d3b8479be35e87ffa215bb9a43 -size 37570 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-6_7_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-5_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-6_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-5_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-6_7_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-5_6_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-6_7_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-6_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-6_7_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ca97e134d3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-6_7_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f1b84d70f67596af405a341d962d83cbd26e3925ffe86af35390e26c7eb0800 +size 27422 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-D-6_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-D-7_7_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-D-6_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-D-7_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-D-6_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-D-7_7_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-D-6_6_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-D-7_7_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-N-6_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-N-7_8_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-N-6_7_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-N-7_8_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-N-6_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-N-7_8_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-N-6_7_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderView-N-7_8_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-7_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-8_8_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-7_7_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-8_8_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-7_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-8_8_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-7_7_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-8_8_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-7_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-8_8_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-7_7_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-8_8_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-7_7_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-8_8_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-7_7_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-8_8_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-7_7_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-8_8_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-7_7_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-D-8_8_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-7_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-7_8_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-7_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-7_8_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-7_8_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-7_8_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-7_8_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-7_8_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-7_8_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-7_8_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png index 688e5a966c..ac13aa8b52 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21f3b6f9ca7105cf06c526666c1c5b03a3ae657c1494d47f90b952ede8b4fa6d -size 39154 +oid sha256:2e7684929f93f437c50549b309e1ba75b95cddb506ad9c8ef97373d0552bddfb +size 39017 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png index ff1ba1ec98..18aaab9fd1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34fbdca09046d303ab1fb2f9e308144c33c91f206dba0644881f1e93f082254b -size 45863 +oid sha256:56f71535fd676a86e79b0e987701d34c85c96276e5338f77117b13e1b3a05144 +size 45715 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png index 0088642b14..9098eef49b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4dfb1ebfa53d7679ffa8f337e245bf646722ccc249a6dc10fc11678c253b2e2 -size 39576 +oid sha256:5554661aeb29ec0d95464942da03df5e83dc78762bc08e2f57851a8f7e811a5a +size 39438 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png index f25f7c2c94..0c6cb3114f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16e02395cb95b49cafe8dd059391c9c5af3b7d309cf2f0130be639b305660f35 -size 39951 +oid sha256:d398070330d5c66358b7ac17d4e1b93795fb9001ffc6eabe533d3bae86de7d58 +size 39810 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png index b1df5a8b0b..74448d6967 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84249343fb7b3d5d781f7047eb739250d20a4d04256b86954c6e5add5e9c3ecb -size 41335 +oid sha256:979138753000ada021a67f4bc14a89b912430c4331ac69aa702b2e50c050d7cb +size 41200 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_7,NEXUS_5,1.0,en].png index 6f1edda851..3cb12c4c94 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c748ef4ba3f2ee500b3601c02db600b29a4d0873d62745b0f96e8c576bc7e80 -size 28396 +oid sha256:e06e1a32d8c9d06a307a5116a396a03308d5e5486f911d34002139485e7d9ffe +size 28176 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_8,NEXUS_5,1.0,en].png index 38c71530ef..af7782f5d7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:244659cbe038252d7ef731b984aaf80d78ad6896cd85eb748aff0184d0429339 -size 29064 +oid sha256:85692ea3847fe5a79f955bb71153ccb6e5b24cf451292c7b9d56f26e2eff95b7 +size 28840 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png index 3cdd3cb229..2e051be73c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c39acb5ec22df70d56d04e507f8d0ecb031490c4322b64ccd4349dabfd4be8ff -size 37734 +oid sha256:cb4be22a56036d53f7c394c3829a9f8f09820276d7529ace0d3731965a8a92f9 +size 37556 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png index bbfd08e8eb..ea837fc372 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b34acb187110520020f5c18265f00517a95e79ff4657c121335bed1f3ae0156e -size 43935 +oid sha256:369e8b4e3f741476cfa5ff7e3f9d5d1e1399fcf3494fadb95540129436ef4d68 +size 43744 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png index edc56ab50d..1c028e2b60 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:654a80311b22ea2b303dc96a7cf38e07333f62c1043068267a8e5d534575a2a2 -size 37944 +oid sha256:5cd8fe02340aeb0edb116c0f3d8f07efc1d331849688718782db25e17bb6e1ab +size 37766 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png index 3a0476f309..cb01f9579d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46fea73a701b1023b9d3786ea7f4102bb61c7435ade80762ce3b186322d40241 -size 38165 +oid sha256:9b2aaa54bf065e6496957c3a60d61336f65e6b76f97e5dbc773284ee49fac91d +size 37988 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png index 1a1a6700be..8d45efac8c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9374377e4debb993e824ce4c9a5e270d090987b916622fda1778d07a90a692d -size 39579 +oid sha256:752abe37a810cc8e4b36ae5f4c4aa38c9f22757b6458f272ba991050c69aee53 +size 39409 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_7,NEXUS_5,1.0,en].png index ba54e1f417..811cb7e4cd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e2c175fc97639d27b4d5561966c64611420d84b29e424c82aef4282c455e7d3 -size 26693 +oid sha256:0c53fdbe427ddb7088d9bd8f9b46a1281d145fd6f9ccc3a7de339750e3079075 +size 26498 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_8,NEXUS_5,1.0,en].png index 11f71571f1..ee2fa7b46f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:869edecbb5c61e3c338e604e7b3850f97d473ff7c7885c5426c381c3b69b90bd -size 27721 +oid sha256:5df2d3b6e4698867277712214fa526e08a9bf790140cd000cfe2bf94c70e77f4 +size 27525 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_1,NEXUS_5,1.0,en].png index def8cb3951..33e091b922 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1665aff8e9520d950e694639adbabed73508b3e1099c3b10badf99f3e898ad8c -size 14367 +oid sha256:efde1c7f011e0a2021e7b94daa693e34ea13255d9ad5030bdff6b87edccc0758 +size 12614 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_3,NEXUS_5,1.0,en].png index 0a889f9c1b..7c59953d03 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:864697fe48277060ccb0cb6dff960257646aee3da0aeee78e1778f543c255efe -size 27763 +oid sha256:1ef92d0b6b2000763f5efda39d40e81871f1b35f7d164b30ca267055d7dd8117 +size 26263 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_4,NEXUS_5,1.0,en].png index a89be8423a..8a0c607aef 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0871da9c2d08eedc7679049dfa3baea176cce38da183bb6293a169098844cedf -size 27803 +oid sha256:fdfe2087c1b0697baa2a4d1ed2dff1114066938071a3a7e3164a1cfd092b559c +size 31066 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_5,NEXUS_5,1.0,en].png index a89be8423a..664177707e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0871da9c2d08eedc7679049dfa3baea176cce38da183bb6293a169098844cedf -size 27803 +oid sha256:817e008ff8a7f3619ebf09ccb673c654f1bc1ab8119f8ede919e91020ad228b2 +size 28204 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_7,NEXUS_5,1.0,en].png index a89be8423a..d160cfe6b5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-D-2_2_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0871da9c2d08eedc7679049dfa3baea176cce38da183bb6293a169098844cedf -size 27803 +oid sha256:3fe3542419140ac2bd473b44d369a72b140bb2d7e47323321bc340e98ac78b6f +size 24865 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_1,NEXUS_5,1.0,en].png index 4c267ead54..b6cd97e0b6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01d8e92c318979c4d5eb64f327db77795a752c198841b7ffed39725db7cda40a -size 13345 +oid sha256:18f82098a4c81d7648ee03f02d5fb15f061553b8e35b315ec260090a799086e0 +size 11759 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_3,NEXUS_5,1.0,en].png index a024f7b811..311f6ee899 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f73d955da1959bbfd49caa5d0ac1ed2ef7371067627fed631fa8b0f8b71134e -size 26667 +oid sha256:609cb79eaabd440908359ae01d0e508aebe55e956fc27f460f4f30d8f9105e2d +size 25456 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_4,NEXUS_5,1.0,en].png index b3c44eb0cf..d97646351e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d287619dcfabff315afd3bdfe24aeab99dabc2f641dd7131b775908a7cda146 -size 26653 +oid sha256:5289e07b1e9177cd143c7ad4c28698e398539b41c15d48b295c8080578666740 +size 29502 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_5,NEXUS_5,1.0,en].png index b3c44eb0cf..be12429385 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d287619dcfabff315afd3bdfe24aeab99dabc2f641dd7131b775908a7cda146 -size 26653 +oid sha256:1f0d4d45f42ccaa4188910b03c85de2b79655a86a5e6c9ca32cb4516ad8e0557 +size 25723 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_7,NEXUS_5,1.0,en].png index b3c44eb0cf..1a4a05b147 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d287619dcfabff315afd3bdfe24aeab99dabc2f641dd7131b775908a7cda146 -size 26653 +oid sha256:e967c504776e055c3ea45c0088a2c0636edb99fd6679dfe878eb3733ba116f8f +size 21815 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png index 91d87cca80..8ff9e04c67 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0596b9207380d08c11dd15fca3ffdbc586c4efeb02da4a0c452b9034a4f2635e -size 395473 +oid sha256:97e5ebc64d490011841f813a4804f2ea702b0180c476bcc717bbb33512979d78 +size 114803 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_4,NEXUS_5,1.0,en].png index fae8a6fca3..2f2ac0e0c0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753 -size 4464 +oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0 +size 4462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png index ce9f3bd552..9c2309187b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:365e3b0f92dd56b4404f50861d119fa58435e9d9c887142632f5566e0dc4c9f1 -size 10796 +oid sha256:b91884f47f7789ccb1e22b29be6cfcfa6fa5975614145eedd4d884b867cfcf0f +size 18458 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png index 21df3b6b35..e119b62da1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7213c01580a7c0caa8d63a8540c9f69208ac8d906a70fd6399c4fa5cacb606e -size 10093 +oid sha256:0c0892fa19589139b4d47a514f0a56486ab47f82b94ef9426f8bb623be7cc0cb +size 16786 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_0,NEXUS_5,1.0,en].png index d0febba414..8e8d4282e9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7de22a0f528c66a4ff6527b5ee8c7b1b857b72b8f5965a553fb705e229f6e1a8 -size 45973 +oid sha256:3168e64fb861ddbd160ee09a75bcce7c6f7a1b6d47f73f83915f1af0e152256e +size 45863 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_1,NEXUS_5,1.0,en].png index bd22dca91e..283008f0fd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fe3f7a930f741743708a91d24c370bc35ea1faab62d7a6098b207a0098dc3a9 -size 47489 +oid sha256:de6b85a67d9149cc8e82fb293ec9b81e963588832bc0489b412dfc7a2bb409c2 +size 47387 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_2,NEXUS_5,1.0,en].png index 3aa46246c4..a806573632 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f08281eb1c3a3a92431381e991d121f97147e54cfb96346fd3258aa48fa45ec -size 47014 +oid sha256:b09b37b87bc25d3e5e46cbafbe468cab3d025fc855fc531fc30a08f4b872520a +size 46911 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_3,NEXUS_5,1.0,en].png index cab2191a34..56fdf44aeb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62cab791065e462249b79e7034307732f61d7e0a501fd20a15803cfcb9e3399e -size 45464 +oid sha256:dbed31f6579154ff6674b7201d6f36b2ea0ad10e20a4b59b59c3cc76f95b78bb +size 45370 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_4,NEXUS_5,1.0,en].png index 1f201ecdd1..b414b0ea96 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe43caa4175d6bba9739e460a596fd744245e7cb29ee5da3316bb587326d91f2 -size 38308 +oid sha256:c4d5b41b5ea9986d7dfab84646ccd89f66f8a36ea3ae870d2773a27e22f51e38 +size 38218 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_0,NEXUS_5,1.0,en].png index ef3a71567d..b83003bf73 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a07c3029bd07b64e9dbb1855812f1c171a5b6e40809b390d095862779e411d9a -size 44185 +oid sha256:e5c1354c569a124786bae728a13bcaf878c3a9bafd70074067e51a06f5b9ac18 +size 44094 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_1,NEXUS_5,1.0,en].png index 6e61668d31..a9c91042af 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:858aa3b3765f630687b76e0f7353f933ef8322e38a51d077a298b70eef83b6c5 -size 44947 +oid sha256:ad43f99ac09531dfa53c56317e83fc256b5632c91ac455ca27b7dc62196a9f71 +size 44850 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_2,NEXUS_5,1.0,en].png index 917be0f3e7..ae79ef4871 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca9cae094c6872f537b755053123e5da7d679b00db146c490486f2b32a7db3e4 -size 44540 +oid sha256:692b4493554c2a11b278b8c0421ce3463e883ee9944209bd0f8fc4dc4d325446 +size 44443 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_3,NEXUS_5,1.0,en].png index 8f2f57cafa..74ad437756 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f15e90c5f66e9c2d6d0c36b34265af435028f2c8492b7eb2c028a5733d35a3ff -size 43123 +oid sha256:d7be3c56e9f87e376dc381752109554425febc03d49b4294a84a2d50fee35335 +size 43036 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_4,NEXUS_5,1.0,en].png index 2efe84641f..60c95f487b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7e0da2d4bd3ac31aca608faad11b4234f46322e1df99a67197fa87c1e13d239 -size 34506 +oid sha256:a58d7b8e0a1d631bf222d022d5c8432b654d8c934960d5b09c1864e4c349ddbc +size 34370 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png index d945b72faf..72056b5ed9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f78a54b9592de1a24bac3796f725e781595c32330f3d340d96c196c0e732bb9d -size 53831 +oid sha256:88bdef3999877e5017bfe0e0ead1514e4e6a58abcde0b0167d4b0ad9d4abd1e0 +size 54020 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png index f716e19da3..90b4f4652f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e16e72290141082417b835a836c81f0a8b127412a04e8cb2c2a36ea8c922c2e6 -size 55147 +oid sha256:35f420b550029d7f8b22d73ea0349d2794cc2e5c5f3080799f496774fad7d2ff +size 55440 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_2,NEXUS_5,1.0,en].png index 2969eb54ab..2f2ac0e0c0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:197d1d4dbf331dcfa9583503a52fcd8defab38efc3cd181ba9cd3c9233c48d4f -size 54305 +oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0 +size 4462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png index e0733fd793..1ae57f1414 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b010513e52a549f498f1458db6ebdda6ecf4ad3f886a570a47142cbd8704f90 -size 55907 +oid sha256:3f62a8a4eded0b742e911970837fe1003228834dd07a216a9bdc4acef37aa468 +size 55800 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png index ff4fb0564f..31edf89ccf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20eee7798706209e11f5d59ab816b7a0589481fb7c0059d2559c848cc992e8ef -size 51481 +oid sha256:1314aaf5394d03b5d08eccdb29783ce8d949f44d3eea28ec8ce434b830515304 +size 51662 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..617d8da89b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecdc26ae1b8943734a8ec1020d7ca9ad4a8e570f5295eb568f80ed314585a9a9 +size 51981 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png index fec3bbf05e..58b944edce 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54352c8066ebb4ac6c0079373cc967abcf9c99f04ddcfce0161fff3bfc34ad6c -size 52258 +oid sha256:32ae9c61f8a01bd54b9fb51af5f0dff222f4df0be1003a9c6ad680d20877448b +size 52275 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png index 77c73664d1..e28209550e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:690ffd45d28f6056b835f21c495ff6d959e089d376e1afcb10395d2e8dd487f3 -size 53490 +oid sha256:1785f0fe49a5afd9b152f6ddb51acad7ba1700b2711cf302ef3a490d948fcd96 +size 53618 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_2,NEXUS_5,1.0,en].png index de297a2f71..2f2ac0e0c0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d3f28985abb793ac209764ab6fbdb1856fe0ae391fe79f80bfa81d7050b4d8c -size 52591 +oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0 +size 4462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png index 06610e4888..6162f39468 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84f147d29e31f1d99519e282d69c0dfa24363fc167d2c6eda323590a70d63677 -size 51222 +oid sha256:455b414e5da5d5174a8d87ef37219c9754e3558b39f025d7aab226b041c51096 +size 51305 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png index f8ee215679..3b7c0855e3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab95b8dd9c480e304333f67032ef42c7a66bc717572a328dbf2f9551773552f7 -size 49850 +oid sha256:a0b83b1d37b34cdf0769e83f625d66479638ce402d5c8e76ef40548414d34400 +size 49862 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..500b83a53e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b09d4a60c3d9944cd6d50a8f0a06d5b3522eba7884f3b5e1e6889f93e8dd1794 +size 50026 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png index f5a3b1cdcd..87469e8d76 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ac3db25f807b871f7f40b0eeb791a010773f6f2463579d1cdf8cfb1373e3dc1 -size 38089 +oid sha256:20ed65388204495106f428d0445c3091bac5b953e46931d21d5ee73c5ae876cc +size 37889 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png index 3dbcce9250..4588e60de3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f93c5071a56654287b33b655cfe277e3b0ab93eec48adbde20c01359cce6661 -size 37677 +oid sha256:7bd91af47b7b336b5bd1abf52ebfcd588d29cb8b0bbb67115fbeaac8ba5bb469 +size 37473 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png index 9f6e6c4e00..d9d2d0a9f5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f80b7138fad81e566cc490d97fb7ee6455fe7af93cc7137845b3f2937c64493 -size 37746 +oid sha256:40bddbf21b6ca1d3f2647ff5168bd7b620b36d5cf3c88f15bab148963c3426b7 +size 37530 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png index 22d818108a..11451941bf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c70fad899a6aadac06ce2041223663381d7c9acfa20338c2f2031056e07fad6d -size 35639 +oid sha256:21e918bf1bbc6f024099d7bb8a1f38eaa356a50a244a9348411489a678579afa +size 35444 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png index fc60a36581..d9300615dc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c586177507ef9ea1f27b74b52a82f97c26d3795061baff4b3d67b8eb1a7ca36a -size 35267 +oid sha256:c051bfc9f9eafd112ccc60eb77b7ffbc9d91c493f2b588fbf51476c2be39ab1a +size 35069 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png index dc9d40f345..fe0e7dfff1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa4483201413ada9f3e84851150074f0035a3fdc72ebab48a2cd8c435ea45688 -size 35372 +oid sha256:943800b5c6fbdda0c39569ab44375ec2e4487c54644663fc21417b077ce5b8e3 +size 35181 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_3,NEXUS_5,1.0,en].png index d5fe3105d2..acd834cafd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdaa865f47a54001607d2c796255e0d50ec7f42f3908f9459bc2555af311274b -size 19572 +oid sha256:d5b98b3001442f362540855822813e1d951b3e2bf2bef74189b0182e6cefd6dd +size 36963 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_4,NEXUS_5,1.0,en].png index d5fe3105d2..52e42ff13a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdaa865f47a54001607d2c796255e0d50ec7f42f3908f9459bc2555af311274b -size 19572 +oid sha256:276afc61d84cdd461e00121e9c38756b91a0f422c848772d52492c9dfe3c4890 +size 28494 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_4_null_3,NEXUS_5,1.0,en].png index 26058a7bfe..4bbfb31331 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_4_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_4_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ccb14dda83f03265d2de3fcd8bfe1d88ded896563e06208887e779a1f1a776e -size 20035 +oid sha256:576027f683e1be5c343977e1a2808e2d8b9632db95552b3001bf6cb68eeef70c +size 41501 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_4_null_4,NEXUS_5,1.0,en].png index 26058a7bfe..2827c77e10 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_4_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewLight--2_4_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ccb14dda83f03265d2de3fcd8bfe1d88ded896563e06208887e779a1f1a776e -size 20035 +oid sha256:64aed73e7a31bb58ef07eaee008c0fa2b1820319b180a4627e216e84919cf98a +size 32488 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-D-4_4_null,NEXUS_5,1.0,en].png index c4310e36d4..7a4021f41f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-D-4_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-D-4_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c9708e025f47a0c9ef501b6af19107e7ca1bc0d2d5db20f84a439b1e8f96d30 -size 36809 +oid sha256:1ea39cc532f9e523365e08d836a11d3aef4ffc8a653a95949be6c595c3f8711a +size 36845 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-N-4_5_null,NEXUS_5,1.0,en].png index e040e023bb..0450054b40 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-N-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-N-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c62a7d690d9138c9a4a6bef79c8191832363f9276cfe30dce81348b0e854b27 -size 43031 +oid sha256:30c7cc4bc2c7abe40766edeaac6cd82f0c260f7135cc9964313ccc159011dc6d +size 42280 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_0,NEXUS_5,1.0,en].png index 09058d469a..d1e7ae7489 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29662089bb7ceefe701078605dad8e81ffc34d2adb26cab4149ab371e06edd09 -size 65468 +oid sha256:0e1be8a38303b5d443f8e0911c34497400b1b577b711191c52ad00b67229a7d1 +size 65314 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_1,NEXUS_5,1.0,en].png index e5b59c760c..0cdf38dc42 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f499207805737f1c7304b20f43d3c38dd1cd1a1cd4110cc00eb2d1489dd1fe5b -size 89218 +oid sha256:05e8a1cabb49befa77b2a89c20e83e2f8355242e8868ae075e7967c7a14ca870 +size 89054 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_2,NEXUS_5,1.0,en].png index 09058d469a..d1e7ae7489 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29662089bb7ceefe701078605dad8e81ffc34d2adb26cab4149ab371e06edd09 -size 65468 +oid sha256:0e1be8a38303b5d443f8e0911c34497400b1b577b711191c52ad00b67229a7d1 +size 65314 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_3,NEXUS_5,1.0,en].png index 2e7f4d9872..90ead2e7fe 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22eb689307a7809446964bb6b9ef84f2bef7e228daab9ae80bebd0b7e7d58b99 -size 65457 +oid sha256:3057d4d0d83a51dda75f7701698ed8be2f2a99d01e5e43ab639f59210cc3a9a5 +size 65326 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_4,NEXUS_5,1.0,en].png index 63361643fc..4fbb84a0e6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99f520bcea2e71385ab8c10aa5fe1bbe24f01f625b5c44bea43479ba606e28c3 -size 66595 +oid sha256:952766c0bbe97ba343ebc68966c0148da68bb4dd8c802dd98f937ca8974a314e +size 66408 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_5,NEXUS_5,1.0,en].png index 417c204a70..6389639819 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecaf49554d7c2d5c570dbcc01a922fc0a3b130ede89e69bb0399433b3e0bf808 -size 66978 +oid sha256:44b50286480b8351ace34656d7396e2dab9694fc5a19fd9226d5008b87b2bb30 +size 66794 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_0,NEXUS_5,1.0,en].png index f362c43731..2feff83fa7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b1a6a75105b643dbef0c18f76e19c23b62634c559ffc9b9073d4626795a53a4 -size 68202 +oid sha256:c1e7bd8361314f95f67930a3795066d0028fd64dbafb79c2d699596d95f6db95 +size 67522 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_1,NEXUS_5,1.0,en].png index 468227be19..222acb9e53 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:006c5af03e2b482373bcb478b988e040e63df53fc95c53035cfb55e2ff672894 -size 91389 +oid sha256:5e4fdc7a1af79a5c2113f7c8f27f1c22463fbb3a3dbdddf83f7258ba513ea06a +size 90775 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_2,NEXUS_5,1.0,en].png index f362c43731..2feff83fa7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b1a6a75105b643dbef0c18f76e19c23b62634c559ffc9b9073d4626795a53a4 -size 68202 +oid sha256:c1e7bd8361314f95f67930a3795066d0028fd64dbafb79c2d699596d95f6db95 +size 67522 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_3,NEXUS_5,1.0,en].png index 9e36b4cf00..dacdeac66b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c4303a44b5e8b8ae71f101ba1abf6895683137bd668f249e4bbd9f42f10ccc2 -size 68296 +oid sha256:f630b7f9eae799cfea12bebb5df52c60e8a5a3007d0a7d6febd90c945bd67566 +size 67292 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_4,NEXUS_5,1.0,en].png index 5b2e2fbb3b..4137dcfae9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34cb5aa8b8053a0575ed7e8f792a9dc79b522fdfee1bd5810aa8a49c51c46188 -size 69792 +oid sha256:0c1fc5a06c2399f36ea14f1135be2a98a836485c980f08523dca2b79edbcee78 +size 69111 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_5,NEXUS_5,1.0,en].png index 05076c1d69..3a9f34d9e1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f51f6c4d823e68fe6ba3d39e3c741e75d6ac7bf2fea8e201c321e40a21b6dff8 -size 70126 +oid sha256:b83df3739ab45b9b96461dd56e69befdb7119e3bc68fa141bd6b4086e384a02c +size 69466 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-D-0_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..82a7ca445f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25d69103d52132a0d47f3e5c4feebc4a981e4d382255e312c83973fff4e0b3e8 +size 60658 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-N-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..32059527ae --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_null_SignedOutView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5844f66e9c07ba824fb3d1871281fc385ec7c0898cae0bf8e532bea1ef2278fb +size 58885 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_InfoListItemMolecule-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_InfoListItemMolecule-D_0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_InfoListItemMolecule-D_0_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_InfoListItemMolecule-D_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_InfoListItemMolecule-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_InfoListItemMolecule-N_1_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_InfoListItemMolecule-N_1_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_InfoListItemMolecule-N_1_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_null_InfoListOrganism-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_null_InfoListOrganism-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b98fc16eeb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_null_InfoListOrganism-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:897ee73ef00a3a322a5ab99b6b3d39945e0b62fea72f1099ace542e4cff893cf +size 13104 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_null_InfoListOrganism-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_null_InfoListOrganism-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d703c086a7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_null_InfoListOrganism-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:770f21753ce0d8f85e8e20b7d74f4ff903da693e5ca9704ef6f2430895656454 +size 12437 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom-N_1_null,NEXUS_5,1.0,en].png index ef000bdde3..366b13e42d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15a168c442590b4f3cbf12daf818061a78b6e29c26ec9307be7db53402748c7a -size 71081 +oid sha256:b2d4bd4cfa4d1556682f1b59b080d551ab8a11009755ed85181c00b32005c1ff +size 68722 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_0,NEXUS_5,1.0,en].png index a6024a3965..7b151a7e89 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52d6fc6f88ce1e80dfe48ad83476e70afa5b5499ec24e8a94bbbc4b0e611e16d -size 76231 +oid sha256:ade2748c46ccc2705ff1f3074e279b048519f25f7e0f2985c116e4da7e1adfdc +size 71065 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_1,NEXUS_5,1.0,en].png index 51aed9c9e7..cd4a12174b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4611148bcc9140bf40a63859ec88bac212a9774d81a5fe5abcfea9afec4192bf -size 75130 +oid sha256:9baff29bc497962832f186c65bd82df8484593f279b4fd49c65a942b3b8e2b52 +size 72038 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_2,NEXUS_5,1.0,en].png index 0860041efc..075fb9482d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39473b8fb90fcef316df257c4b3a45aa9c441199ef169b275e660d58d7fae52b -size 72606 +oid sha256:323786260b184088351d98658634c45432209ee6efa99aa6d140408c70ecdb14 +size 67982 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_3,NEXUS_5,1.0,en].png index b5b50230ac..7db5d45a6f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:572db9d4c1796ec37e113907d5c13728cb87ce9959d94bb9396d7ea38914e52b -size 76355 +oid sha256:e5d104097c15d823264d6e2216d9c70ced3ed42968840c53b2c2577db4472d4e +size 72268 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_4,NEXUS_5,1.0,en].png index c7f31854cb..8789a2cccf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ca9d811ba70d8642e4831a1d62b94960c79fb28f93a3c36d67d2d67c1b376fc -size 74706 +oid sha256:26b53f6566ab650e62025a2a2c408bd327b9faa07f622cb5679600039f4257fc +size 70057 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_5,NEXUS_5,1.0,en].png index 81a543bd06..689c652901 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2319d9245e6f218f0b77d419d99ba101eda112184fc70729633006da0a4ee03d -size 74372 +oid sha256:4d392afe26f6ac4294488cd4e9f4fb33dbf3e12a418053b2e4184475bfbe97f4 +size 69399 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_6,NEXUS_5,1.0,en].png index 781cf6f3a6..33e7c6e3e6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6131175dabf7e1835856d37c37f8cdacaaa7294b44d4676a0bd4d32d4d2a1337 -size 79118 +oid sha256:c4a4754541af2817156fd1aba7e65f9b9ea48de68f3142646fb0dedc4a4d1073 +size 72811 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_7,NEXUS_5,1.0,en].png index 9f56ea678a..f6d4d064f6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_BloomInitials-N_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce7f7219bd9027f0e6f3d8b44a4170b1e058c549cc143fbf63d67eab3e0eee2b -size 69351 +oid sha256:1d16d96dcf90fb83c8dd870da0c70b53b6f7655998e362b76811e4ce7092de8c +size 66529 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_0,NEXUS_5,1.0,en].png index d06c97ace6..682b614df0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bf9d2e16fa86ec87e38fd82ecde90176542057d58729837d8aee73eb4828d12 -size 63936 +oid sha256:7c50b77630a1fbeef982fc61c8003b61f9468e2d20eb83461c5544598fe320a2 +size 63841 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_1,NEXUS_5,1.0,en].png index f0e507f655..352ac80480 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6575825152b070f8c2f6abe5ab89f5d1426d76df908638f3934c217193f1aac -size 58297 +oid sha256:ad7d52dec927841c7b05b447c85cec5dd22715ab51eb9ac58408a5e5cd58152c +size 60607 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_2,NEXUS_5,1.0,en].png index 4f5be56d3f..949ed18a4c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17d225fbb3e9d7f1a954c0bc94b94cc1d7f131d226347b32c30940ba03991154 -size 67928 +oid sha256:ed35ba6ea3c1b59c325e6e87682f65d37030ce22b21f1971a23b582f7920f008 +size 67287 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_3,NEXUS_5,1.0,en].png index cd121a5eb5..51bf08207a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f05a11dcd4174a1784d6c7fcd32154a1064d0a3369b18c857f051ef72c09943 -size 64470 +oid sha256:4f319642073688178211971992ea9f8f148056bdcf32d1784c3f5058d738908e +size 63928 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_4,NEXUS_5,1.0,en].png index 053b94e8a0..39849a8444 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e6b22c6a7acbdbc77ebad06831318467ad40bdfbe8c56a0a81e8e9d69de4ff0 -size 66145 +oid sha256:77fcc8873a0e2f9bb2b8199aef58cb4477a4997930d84417dd689da183faa1c8 +size 66073 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_5,NEXUS_5,1.0,en].png index a51c3b1758..f142fd872c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29fd768d347c14c0e9f99de9e2c4e341f76850b8447ccd30fb240590bc8cd706 -size 68885 +oid sha256:116672eb16c9ce26568e84167137d96de3cf846e7516831976e1a66287f24983 +size 67517 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_6,NEXUS_5,1.0,en].png index 5328ba5fe0..8c17959d1c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e18c010bfd1cf93b5b30cf38b58b3cc24d3e1fdb992050786e35004d112563f -size 61822 +oid sha256:47571cc16e4ca99e8d00ad19f8bafa16a94f3b263ad71fbaeec70dbaa2dcbaed +size 61781 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_7,NEXUS_5,1.0,en].png index 46e82926c4..33018c3a2b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_BloomInitials_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21465f36c39930d08edbe2b099ff0cd2765292ee29a724720f75760066845440 -size 67954 +oid sha256:a62203cc72ab5e0619ad353f6741a3c26f7a3f72ea4b263c1a8539028f43359b +size 68141 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_Bloom_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_Bloom_0_null,NEXUS_5,1.0,en].png index 9858f5866f..021d782909 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_Bloom_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_null_Bloom_Bloom_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e58cd709cb5e5d3c49300c0a3818fa31bf17f610059b3e9624737cefe3d113fa -size 77800 +oid sha256:f81ce6f66e9d1054b6c272b64fa9c9859f4a21e9f94839f2a58fec49476868c8 +size 77558 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_0_75f_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_0_75f__0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_0_75f_0_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_0_75f__0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_1_0f_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_1_0f__0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_1_0f_0_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_1_0f__0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_1_5f_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_1_5f_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 6f7eca2dec..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_1_5f_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c6ac3ab7ac24942fca51adfc3cc8bf5ad797e912baf835baa73e8289b6d4a94d -size 28308 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_1_5f__0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_1_5f__0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7335dae68d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.text_null_DpScale_1_5f__0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54244cea4f74dbc5b78360f5ca58807532145e63c4d5d8d2faf057a9c1fc6a3a +size 28270 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_DatePickerDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_DatePickerDark_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..938e97d2af --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_DatePickerDark_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00b8a072864b530c3b736b24254775f4c3885a66d6fadc0a09b797dfa685757c +size 32407 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_DatePickerLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_DatePickerLight_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..91266d7cde --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_DatePickerLight_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ab11e7dd119809b04dcc3d495a78404ac8fb28186d5080f69d9c024a09555a2 +size 33676 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_DatePickerPreviewDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_DatePickerPreviewDark_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index c839472d5c..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_DatePickerPreviewDark_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2776f6426818ed5faa46dc877848940a91b68773ee58a9a42490b659c865d1ca -size 33154 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_DatePickerPreviewLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_DatePickerPreviewLight_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index aa9d0c1950..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_DatePickerPreviewLight_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5a2003610e82a97d359cf009321c2e41cfe40f4fe48604637d6d3b966b21f5ab -size 34435 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalPreviewDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalDark_0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalPreviewDark_0_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalDark_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalPreviewLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalLight_0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalPreviewLight_0_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components.previews_null_DateTimepickers_TimePickerVerticalLight_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..182cc3a083 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c5bd25caa8c462fd9a98fef14f011450adb414ec14d1a8b285d37692185442c +size 5616 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..86a758d3b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84d00d9738ffbd2b3798e3c31b13d4a086377abaf250049aca24fdfef1132ed8 +size 5459 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveEmptyQuery_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarActiveEmptyQuery_0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveEmptyQuery_0_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarActiveEmptyQuery_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithContent_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarActiveWithContent_0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithContent_0_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarActiveWithContent_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithNoResults_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarActiveWithNoResults_0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithNoResults_0_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarActiveWithNoResults_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithQueryNoBackButton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarActiveWithQueryNoBackButton_0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithQueryNoBackButton_0_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarActiveWithQueryNoBackButton_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithQuery_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarActiveWithQuery_0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarPreviewActiveWithQuery_0_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarActiveWithQuery_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarPreviewInactive_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarInactive_0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarPreviewInactive_0_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Searchviews_SearchBarInactive_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-D-7_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-D-7_7_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d90adf9d9e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-D-7_7_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:377c36c5f1019bb6838749fd93c91dec703da825f75e5600ef028d2aaeaa79ac +size 5646 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-N-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-N-7_8_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..073873fe76 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-N-7_8_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bbe1dbe62743ca682a48e9e777e8b2b09064afeb2ce36448a62452faaca5d67 +size 5629 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-D-8_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-D-8_8_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ffb2d2a6e1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-D-8_8_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb2d844445877bdd615c4b9fcedd9759ccf4fa7e72b8a740a74af18f8b7d18f8 +size 5925 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-N-8_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-N-8_9_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..94f6a515a8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-N-8_9_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e3fb36aa1c559c6d3ae8ca2399531b5090229131ba23b710d46639d3a26b6ec +size 5847 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-7_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-9_9_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-7_7_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-9_9_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-9_10_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-7_8_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-9_10_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-10_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-10_10_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d32c73e543 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-10_10_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60410832f66d0c70166443d855743ef956b1b97a3684624ce5797805d71e04f6 +size 8712 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-10_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-10_11_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..93591c72a9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-10_11_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54c5208fd88e1f404667cdc0b39ff87824115a1c414dfdbd55a334fa6a445548 +size 8619 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-11_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-11_11_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..635fbd2b36 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-11_11_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b43968125ef27b3b9918251590befe6e6a7d781342e8d915833fa1cba395f17d +size 7211 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-11_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-11_12_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..361795f953 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-11_12_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7ea766f311321684f8a387a7be9e2f88a4f27a4adb85a9b0f4d0b3e56854ee0 +size 7034 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_2_null,NEXUS_5,1.0,en].png index ce58d7b6ab..e6274d3ab9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3a8bb9cbb98e7acf4d5e6aee29071ce851229622ada00723336f1167430f123 -size 13730 +oid sha256:a0c5c53f53eb3cdda73391fabda2658b5c009e9a13761ef9acae37530f3cb1b5 +size 14017 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_3_null,NEXUS_5,1.0,en].png index 55268f535a..59f70b2c3c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38e673e6cb326662096558fc9f25b5b84835004217ae06d2be75e9d6232e0ddf -size 12893 +oid sha256:e5b786b07d92459399099e0f7730804de2f2123d8c5eb51ca64f7609768ecc34 +size 13157 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_1_null,NEXUS_5,1.0,en].png index 87893f6777..bfc1d62dbc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:463156fde1b2766a5c17f0e4aaac79243496d832ac115d090ec212e0077c4b4a -size 41560 +oid sha256:54f6712d9cbd60c6a6d108ddcff08492a5e74cbba7d1f4b5f3312a068e4dd798 +size 43207 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_2_null,NEXUS_5,1.0,en].png index 000ebf17b9..e30b7ac05a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f490d44a0609fac5f4ae08ec20472cf55ca90bbd7b4c719087b47be4d4e9ab14 -size 39270 +oid sha256:e156ed6831abb52d8bbd95934517f423d0cae06e21e9266b099174aad55228ab +size 40662 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_3_null,NEXUS_5,1.0,en].png index 5e4d0a391d..d47caa7de0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16009c30863c10bda995eb570637d754075703d2bcce95b366e34667a5d53671 -size 84173 +oid sha256:fe441727d1a64a7439a15780a208b50bfacd875d6a6fa9bee980508e9beda5a7 +size 87262 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_4_null,NEXUS_5,1.0,en].png index d602d87983..a42ece91ed 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:179ee5b9d4d8df4a54c09e7406ec006b3bc7407ce27daa2d69369ca89cee373d -size 80573 +oid sha256:031e8632fb034425c6e30801abb5f57271e5411784501ee27cd413db363e2d67 +size 83036 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_0_null,NEXUS_5,1.0,en].png index 360c35f3b1..a6f891a3a0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da51cd385e731b4544488be10e9a71f79714539d7cbcaaa0b760f3e2b3cf9272 -size 45199 +oid sha256:fd38c36b9b85c3ca3e290e2c0fd338ca52552398d2497a92a573ffd3f133b567 +size 47937 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_1_null,NEXUS_5,1.0,en].png index 0462ba7ca3..c3d1dc9ae0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1624403c6bbd12925b84551215c1ea3a2d3073375d3422ee1ffd5578a3635659 -size 42351 +oid sha256:033292295440c19363ebf1cc425c60226cf6f811a25219ae7939c8d2871091c4 +size 45020 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.theme_null_ColorsSchemePreviewDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.theme_null_ColorsSchemeDark_0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.theme_null_ColorsSchemePreviewDark_0_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.theme_null_ColorsSchemeDark_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.theme_null_ColorsSchemePreviewLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.theme_null_ColorsSchemeLight_0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.theme_null_ColorsSchemePreviewLight_0_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.theme_null_ColorsSchemeLight_0_null,NEXUS_5,1.0,en].png diff --git a/tools/lint/lint.xml b/tools/lint/lint.xml index 715131226a..4da7a0bba3 100644 --- a/tools/lint/lint.xml +++ b/tools/lint/lint.xml @@ -124,4 +124,7 @@ + + + diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 0d410fc9c4..7e07d269c0 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -25,6 +25,12 @@ "screen_onboarding_.*" ] }, + { + "name": ":features:signedout:impl", + "includeRegex": [ + "screen_signed_out_.*" + ] + }, { "name": ":features:invitelist:impl", "includeRegex": [ @@ -138,6 +144,7 @@ { "name": ":features:preferences:impl", "includeRegex": [ + "screen_advanced_settings_.*", "screen_edit_profile_.*" ] }, diff --git a/tools/templates/file_templates.zip b/tools/templates/file_templates.zip deleted file mode 100644 index 7352ac3074..0000000000 Binary files a/tools/templates/file_templates.zip and /dev/null differ diff --git a/tools/templates/files/IntelliJ IDEA Global Settings b/tools/templates/files/IntelliJ IDEA Global Settings new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/templates/files/fileTemplates/Template Module Feature Build Gradle API.kts b/tools/templates/files/fileTemplates/Template Module Feature Build Gradle API.kts new file mode 100644 index 0000000000..5c72896315 --- /dev/null +++ b/tools/templates/files/fileTemplates/Template Module Feature Build Gradle API.kts @@ -0,0 +1,11 @@ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.${MODULE_NAME}.api" +} + +dependencies { + implementation(projects.libraries.architecture) +} diff --git a/tools/templates/files/fileTemplates/Template Module Feature Build Gradle Impl.kts b/tools/templates/files/fileTemplates/Template Module Feature Build Gradle Impl.kts new file mode 100644 index 0000000000..6b40a1d06f --- /dev/null +++ b/tools/templates/files/fileTemplates/Template Module Feature Build Gradle Impl.kts @@ -0,0 +1,34 @@ +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.${MODULE_NAME}.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + api(projects.features.${MODULE_NAME}.api) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + + ksp(libs.showkase.processor) +} diff --git a/tools/templates/files/fileTemplates/Template Module Feature Entry Point API.kt b/tools/templates/files/fileTemplates/Template Module Feature Entry Point API.kt new file mode 100644 index 0000000000..6297ec4e24 --- /dev/null +++ b/tools/templates/files/fileTemplates/Template Module Feature Entry Point API.kt @@ -0,0 +1,20 @@ +package io.element.android.features.${MODULE_NAME}.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint + +interface ${FEATURE_NAME}EntryPoint : FeatureEntryPoint { + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + // Add your callbacks + } +} diff --git a/tools/templates/files/fileTemplates/Template Module Feature Entry Point Flow Impl.kt b/tools/templates/files/fileTemplates/Template Module Feature Entry Point Flow Impl.kt new file mode 100644 index 0000000000..adfd142ae5 --- /dev/null +++ b/tools/templates/files/fileTemplates/Template Module Feature Entry Point Flow Impl.kt @@ -0,0 +1,30 @@ +package io.element.android.features.${MODULE_NAME}.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.${MODULE_NAME}.api.${FEATURE_NAME}EntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class Default${FEATURE_NAME}EntryPoint @Inject constructor() : ${FEATURE_NAME}EntryPoint { + + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): ${FEATURE_NAME}EntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : ${FEATURE_NAME}EntryPoint.NodeBuilder { + + override fun callback(callback: ${FEATURE_NAME}EntryPoint.Callback): ${FEATURE_NAME}EntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode<${FEATURE_NAME}FlowNode>(buildContext, plugins) + } + } + } +} diff --git a/tools/templates/files/fileTemplates/Template Module Feature Node Flow Impl.kt b/tools/templates/files/fileTemplates/Template Module Feature Node Flow Impl.kt new file mode 100644 index 0000000000..d08d67ae38 --- /dev/null +++ b/tools/templates/files/fileTemplates/Template Module Feature Node Flow Impl.kt @@ -0,0 +1,57 @@ +package io.element.android.features.${MODULE_NAME}.impl + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +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.libraries.architecture.BackstackNode +import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import kotlinx.parcelize.Parcelize + +// CHANGE THE SCOPE +@ContributesNode(AppScope::class) +class ${FEATURE_NAME}FlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : BackstackNode<${FEATURE_NAME}FlowNode.NavTarget>( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins, +) { + + sealed interface NavTarget : Parcelable { + @Parcelize + object Root : NavTarget + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Root -> { + //Give your root node or completely delete this FlowNode if you have only one node. + createNode<>(buildContext) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = backstack, + modifier = modifier, + transitionHandler = rememberDefaultTransitionHandler(), + ) + } +} diff --git a/tools/templates/files/fileTemplates/Template Presentation Classes.kt b/tools/templates/files/fileTemplates/Template Presentation Classes.kt new file mode 100644 index 0000000000..aa44bc4269 --- /dev/null +++ b/tools/templates/files/fileTemplates/Template Presentation Classes.kt @@ -0,0 +1,22 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end + +import androidx.compose.runtime.Composable +import io.element.android.libraries.architecture.Presenter +import javax.inject.Inject + +class ${NAME}Presenter @Inject constructor() : Presenter<${NAME}State> { + + @Composable + override fun present(): ${NAME}State { + + fun handleEvents(event: ${NAME}Events) { + when (event) { + ${NAME}Events.MyEvent -> Unit + } + } + + return ${NAME}State( + eventSink = ::handleEvents + ) + } +} diff --git a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.0.kt b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.0.kt new file mode 100644 index 0000000000..26372fc970 --- /dev/null +++ b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.0.kt @@ -0,0 +1,15 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class ${NAME}StateProvider : PreviewParameterProvider<${NAME}State> { + override val values: Sequence<${NAME}State> + get() = sequenceOf( + a${NAME}State(), + // Add other states here + ) +} + +fun a${NAME}State() = ${NAME}State( + eventSink = {} +) diff --git a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.1.kt b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.1.kt new file mode 100644 index 0000000000..0c997351f0 --- /dev/null +++ b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.1.kt @@ -0,0 +1,29 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.di.AppScope + +// CHANGE THE SCOPE +@ContributesNode(AppScope::class) +class ${NAME}Node @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: ${NAME}Presenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ${NAME}View( + state = state, + modifier = modifier + ) + } +} diff --git a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.2.kt b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.2.kt new file mode 100644 index 0000000000..0080f8d905 --- /dev/null +++ b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.2.kt @@ -0,0 +1,35 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun ${NAME}View( + state: ${NAME}State, + modifier: Modifier = Modifier, +) { + Box(modifier, contentAlignment = Alignment.Center) { + Text( + "${NAME} feature view", + color = MaterialTheme.colorScheme.primary, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun ${NAME}ViewPreview( + @PreviewParameter(${NAME}StateProvider::class) state: ${NAME}State +) = ElementPreview { + ${NAME}View( + state = state, + ) +} diff --git a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.3.kt b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.3.kt new file mode 100644 index 0000000000..3fcdd7f219 --- /dev/null +++ b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.3.kt @@ -0,0 +1,7 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end + +// TODO add your ui models. Remove the eventSink if you don't have events. +// Do not use default value, so no member get forgotten in the presenters. +data class ${NAME}State( + val eventSink: (${NAME}Events) -> Unit +) diff --git a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.4.kt b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.4.kt new file mode 100644 index 0000000000..4b47069304 --- /dev/null +++ b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.4.kt @@ -0,0 +1,6 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end + +// TODO Add your events or remove the file completely if no events +sealed interface ${NAME}Events { + data object MyEvent: ${NAME}Events +} diff --git a/tools/templates/files/options/file.template.settings.xml b/tools/templates/files/options/file.template.settings.xml new file mode 100644 index 0000000000..c7d26d1fb7 --- /dev/null +++ b/tools/templates/files/options/file.template.settings.xml @@ -0,0 +1,18 @@ + + + +