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.
+
+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.
+
+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.
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/templates/generate_templates.sh b/tools/templates/generate_templates.sh
new file mode 100755
index 0000000000..9b59ea69f8
--- /dev/null
+++ b/tools/templates/generate_templates.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+#
+# Copyright 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.
+#
+
+echo "Zipping the contents of the 'files' directory..."
+
+# Ensure tmp folder exists
+mkdir -p tmp
+
+rm -f ./tmp/file_templates.zip
+pushd ./tools/templates/files
+zip -r ../../../tmp/file_templates.zip .
+popd