From 5cee2121ff76fa5b48e063d000f1e6fa732bedb3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 May 2023 11:11:45 +0000 Subject: [PATCH 01/34] Update dependency io.element.android:wysiwyg to v2.2.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 76435c8e18..536c9f9655 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -148,7 +148,7 @@ anvil_compiler_api = { module = "com.squareup.anvil:compiler-api", version.ref = anvil_compiler_utils = { module = "com.squareup.anvil:compiler-utils", version.ref = "anvil" } # Composer -wysiwyg = "io.element.android:wysiwyg:2.1.0" +wysiwyg = "io.element.android:wysiwyg:2.2.0" # Miscellaneous # Add unused dependency to androidx.compose.compiler:compiler to let Renovate create PR to change the From 8beb7310efc4b88630ab9500c36a28cda9754bbb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 May 2023 11:55:23 +0200 Subject: [PATCH 02/34] Fix compilation issue with Wysiwyg 2.2.0 --- .../android/libraries/textcomposer/RichTextComposerLayout.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/RichTextComposerLayout.kt b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/RichTextComposerLayout.kt index 2fe81b44ac..e5ab34ebfe 100644 --- a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/RichTextComposerLayout.kt +++ b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/RichTextComposerLayout.kt @@ -47,7 +47,7 @@ import io.element.android.libraries.textcomposer.databinding.ComposerRichTextLay import io.element.android.libraries.textcomposer.databinding.ViewRichTextMenuButtonBinding import io.element.android.libraries.textcomposer.tools.setTextIfDifferent import io.element.android.wysiwyg.EditorEditText -import io.element.android.wysiwyg.inputhandlers.models.InlineFormat +import io.element.android.wysiwyg.view.models.InlineFormat import uniffi.wysiwyg_composer.ActionState import uniffi.wysiwyg_composer.ComposerAction import io.element.android.libraries.resources.R as ElementR From f1bd7671123b8e16d551d99bde4304d7a248595c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 May 2023 11:56:08 +0200 Subject: [PATCH 03/34] Fix compilation warnings. --- .../element/android/libraries/matrix/impl/RustMatrixClient.kt | 3 +++ .../android/libraries/push/impl/push/DefaultPushHandler.kt | 3 --- .../io/element/android/libraries/statemachine/StateMachine.kt | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) 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 57dc8c2fc9..3909aafa94 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 @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.libraries.matrix.impl import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -44,6 +46,7 @@ import io.element.android.libraries.matrix.impl.verification.RustSessionVerifica import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.filter 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 09afe0a861..9b6be804c9 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 @@ -105,9 +105,6 @@ class DefaultPushHandler @Inject constructor( Timber.tag(loggerTag.value).d("## handleInternal()") } - pushData.roomId ?: return - pushData.eventId ?: return - val clientSecret = pushData.clientSecret val userId = if (clientSecret == null) { // Should not happen. In this case, restore default session diff --git a/libraries/statemachine/src/main/kotlin/io/element/android/libraries/statemachine/StateMachine.kt b/libraries/statemachine/src/main/kotlin/io/element/android/libraries/statemachine/StateMachine.kt index ccb98e0563..dd8ea7114d 100644 --- a/libraries/statemachine/src/main/kotlin/io/element/android/libraries/statemachine/StateMachine.kt +++ b/libraries/statemachine/src/main/kotlin/io/element/android/libraries/statemachine/StateMachine.kt @@ -60,6 +60,7 @@ class StateMachine( currentStateConfig?.onEnter?.invoke(nextState) } + @Suppress("UNCHECKED_CAST") private fun findMatchingRoute(event: E): StateMachineRoute? { val routesForEvent = routes.filter { it.eventType.isInstance(event) } From 0ac0bbb37af6db22e68cfb05abcd20aee0e8c417 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 May 2023 12:25:07 +0200 Subject: [PATCH 04/34] Create the mockked Bitmap in the constructor, to remove increased time out. --- .../detection/RageshakeDetectionPresenterTest.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt index 869f0412cd..55482c2526 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt @@ -32,9 +32,11 @@ import io.mockk.mockk import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Test -import kotlin.time.Duration.Companion.seconds class RageshakeDetectionPresenterTest { + + private val aBitmap: Bitmap = mockk() + @Test fun `present - initial state`() = runTest { val screenshotHolder = FakeScreenshotHolder(screenshotUri = null) @@ -85,7 +87,7 @@ class RageshakeDetectionPresenterTest { } @Test - fun `present - screenshot with success then dismiss`() = runTest(timeout = 30.seconds) { + fun `present - screenshot with success then dismiss`() = runTest { val screenshotHolder = FakeScreenshotHolder(screenshotUri = null) val rageshake = FakeRageShake(isAvailableValue = true) val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true) @@ -108,7 +110,7 @@ class RageshakeDetectionPresenterTest { rageshake.triggerPhoneRageshake() assertThat(awaitItem().takeScreenshot).isTrue() initialState.eventSink.invoke( - RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Success(aBitmap())) + RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Success(aBitmap)) ) assertThat(awaitItem().showDialog).isTrue() initialState.eventSink.invoke(RageshakeDetectionEvents.Dismiss) @@ -153,7 +155,7 @@ class RageshakeDetectionPresenterTest { } @Test - fun `present - screenshot then disable`() = runTest(timeout = 1.seconds) { + fun `present - screenshot then disable`() = runTest { val screenshotHolder = FakeScreenshotHolder(screenshotUri = null) val rageshake = FakeRageShake(isAvailableValue = true) val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true) @@ -176,7 +178,7 @@ class RageshakeDetectionPresenterTest { rageshake.triggerPhoneRageshake() assertThat(awaitItem().takeScreenshot).isTrue() initialState.eventSink.invoke( - RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Success(aBitmap())) + RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Success(aBitmap)) ) assertThat(awaitItem().showDialog).isTrue() initialState.eventSink.invoke(RageshakeDetectionEvents.Disable) @@ -187,5 +189,3 @@ class RageshakeDetectionPresenterTest { } } -private fun aBitmap(): Bitmap = mockk() - From 5cf17a432f344e56755957a9f4185e03265aedaa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 May 2023 12:46:05 +0200 Subject: [PATCH 05/34] Init the mockked Bitmap only once. --- .../impl/detection/RageshakeDetectionPresenterTest.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt index 55482c2526..eb49eb450e 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt @@ -31,11 +31,20 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.mockk.mockk import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest +import org.junit.BeforeClass import org.junit.Test class RageshakeDetectionPresenterTest { - private val aBitmap: Bitmap = mockk() + companion object { + private lateinit var aBitmap: Bitmap + + @BeforeClass + @JvmStatic + fun initBitmap() { + aBitmap = mockk() + } + } @Test fun `present - initial state`() = runTest { From bd242aee64dfe162eebbf7703d6f9cfc3941f436 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 24 May 2023 11:12:14 +0100 Subject: [PATCH 06/34] Fix bad preview name for invite list --- .../android/features/invitelist/impl/InviteListView.kt | 4 ++-- ...up_InviteListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png} | 0 ...up_InviteListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png} | 0 ...up_InviteListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png} | 0 ...up_InviteListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png} | 0 ...up_InviteListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png} | 0 ...up_InviteListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png} | 0 ...p_InviteListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png} | 0 ...p_InviteListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png} | 0 ...p_InviteListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png} | 0 ...p_InviteListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png} | 0 ...p_InviteListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png} | 0 ...p_InviteListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png} | 0 13 files changed, 2 insertions(+), 2 deletions(-) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png} (100%) 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 d714eda319..b6e0f59667 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 @@ -161,12 +161,12 @@ fun InviteListContent( @Preview @Composable -internal fun RoomListViewLightPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) = +internal fun InviteListViewLightPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) = ElementPreviewLight { ContentToPreview(state) } @Preview @Composable -internal fun RoomListViewDarkPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) = +internal fun InviteListViewDarkPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) = ElementPreviewDark { ContentToPreview(state) } @Composable diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_InviteListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png From 4780bec25cbf8302b146c811c7067a8f7a197cdd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 15:37:07 +0000 Subject: [PATCH 07/34] Update dependency io.nlopez.compose.rules:detekt to v0.1.7 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3855f48400..8657aa7b4c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,7 +59,7 @@ allprojects { config = files("$rootDir/tools/detekt/detekt.yml") } dependencies { - detektPlugins("io.nlopez.compose.rules:detekt:0.1.6") + detektPlugins("io.nlopez.compose.rules:detekt:0.1.7") } // KtLint From 03bbd64780dc8a0ead830ec02d89cf6c4706be69 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 17:09:00 +0000 Subject: [PATCH 08/34] Update dependency io.element.android:wysiwyg to v2.2.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0c120fa295..86ed6644b0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -148,7 +148,7 @@ anvil_compiler_api = { module = "com.squareup.anvil:compiler-api", version.ref = anvil_compiler_utils = { module = "com.squareup.anvil:compiler-utils", version.ref = "anvil" } # Composer -wysiwyg = "io.element.android:wysiwyg:2.2.0" +wysiwyg = "io.element.android:wysiwyg:2.2.1" # Miscellaneous # Add unused dependency to androidx.compose.compiler:compiler to let Renovate create PR to change the From 796d78a203cd3d37c0fc6dac9c8c1320b3984b78 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 20:29:04 +0000 Subject: [PATCH 09/34] Update activity to v1.7.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0c120fa295..8bee730e1a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ datastore = "1.0.0" constraintlayout = "2.1.4" recyclerview = "1.3.0" lifecycle = "2.6.1" -activity = "1.7.1" +activity = "1.7.2" startup = "1.1.1" # Compose From 0dee0784ba7ca76e532e47075b08e250ee2a8a56 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 25 May 2023 08:42:44 +0200 Subject: [PATCH 10/34] Room list contextual menu (#427) - Adds `ModalBottomSheet` to our design components (it wraps the homonimous Material3 one). - Adds a bottom sheet to the Room list using the aforementioned design component. - Adds navigation from the room list to a room detail (context menu "Settings" action). - Consolidates the "leave room flow" into a new `leaveroom` module used by both the room list and the room details. - Adds progress indicator to the leave room flow - Uses new `leaveroom` module in `roomdetails` module too. Parent issue: - https://github.com/vector-im/element-x-android/issues/261 --- .../android/appnav/LoggedInFlowNode.kt | 11 +- .../io/element/android/appnav/RoomFlowNode.kt | 4 +- changelog.d/427.feature | 1 + features/leaveroom/api/build.gradle.kts | 31 +++ .../features/leaveroom/api/LeaveRoomEvent.kt | 26 ++ .../leaveroom/api/LeaveRoomPresenter.kt | 25 ++ .../features/leaveroom/api/LeaveRoomState.kt | 43 +++ .../leaveroom/api/LeaveRoomStateProvider.kt | 58 ++++ .../features/leaveroom/api/LeaveRoomView.kt | 131 +++++++++ features/leaveroom/fake/build.gradle.kts | 44 ++++ .../leaveroom/fake/LeaveRoomPresenterFake.kt | 44 ++++ .../fake/LeaveRoomPresenterFakeModule.kt | 30 +++ features/leaveroom/impl/build.gradle.kts | 46 ++++ .../leaveroom/impl/LeaveRoomPresenterImpl.kt | 127 +++++++++ .../impl/LeaveRoomPresenterImplModule.kt | 30 +++ .../impl/LeaveRoomPresenterImplTest.kt | 248 ++++++++++++++++++ features/roomdetails/impl/build.gradle.kts | 2 + .../roomdetails/impl/RoomDetailsEvent.kt | 4 +- .../roomdetails/impl/RoomDetailsPresenter.kt | 57 +--- .../roomdetails/impl/RoomDetailsState.kt | 24 +- .../impl/RoomDetailsStateProvider.kt | 4 +- .../roomdetails/impl/RoomDetailsView.kt | 44 +--- .../roomdetails/RoomDetailsPresenterTests.kt | 106 +------- .../roomlist/api/RoomListEntryPoint.kt | 1 + features/roomlist/impl/build.gradle.kts | 2 + .../roomlist/impl/RoomListContextMenu.kt | 148 +++++++++++ .../features/roomlist/impl/RoomListEvents.kt | 6 + .../features/roomlist/impl/RoomListNode.kt | 9 +- .../roomlist/impl/RoomListPresenter.kt | 16 ++ .../features/roomlist/impl/RoomListState.kt | 16 +- .../roomlist/impl/RoomListStateProvider.kt | 6 + .../features/roomlist/impl/RoomListView.kt | 57 +++- .../impl/components/RoomSummaryRow.kt | 16 +- .../impl/model/RoomListRoomSummary.kt | 2 +- .../model/RoomListRoomSummaryPlaceholders.kt | 3 +- .../roomlist/impl/search/RoomListSearch.kt | 15 +- .../roomlist/impl/RoomListPresenterTests.kt | 94 +++++++ .../libraries/designsystem/VectorIcons.kt | 1 + .../theme/components/ModalBottomSheet.kt | 103 ++++++++ .../src/main/res/drawable/ic_door_open_24.xml | 10 + samples/minimal/build.gradle.kts | 1 + .../android/samples/minimal/RoomListScreen.kt | 12 +- ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...tentDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...entLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_8,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_8,NEXUS_5,1.0,en].png | 3 + ...heetDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...eetLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + 60 files changed, 1462 insertions(+), 250 deletions(-) create mode 100644 changelog.d/427.feature create mode 100644 features/leaveroom/api/build.gradle.kts create mode 100644 features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt create mode 100644 features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomPresenter.kt create mode 100644 features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt create mode 100644 features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt create mode 100644 features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt create mode 100644 features/leaveroom/fake/build.gradle.kts create mode 100644 features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFake.kt create mode 100644 features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFakeModule.kt create mode 100644 features/leaveroom/impl/build.gradle.kts create mode 100644 features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt create mode 100644 features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplModule.kt create mode 100644 features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt create mode 100644 libraries/designsystem/src/main/res/drawable/ic_door_open_24.xml create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentLightPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_8,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_BottomSheets_ModalBottomSheetDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_BottomSheets_ModalBottomSheetLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index b8c2c7742a..62c58caadb 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -135,7 +135,10 @@ class LoggedInFlowNode @AssistedInject constructor( object RoomList : NavTarget @Parcelize - data class Room(val roomId: RoomId) : NavTarget + data class Room( + val roomId: RoomId, + val initialElement: RoomFlowNode.NavTarget = RoomFlowNode.NavTarget.Messages + ) : NavTarget @Parcelize object Settings : NavTarget @@ -176,6 +179,10 @@ class LoggedInFlowNode @AssistedInject constructor( override fun onInvitesClicked() { backstack.push(NavTarget.InviteList) } + + override fun onRoomSettingsClicked(roomId: RoomId) { + backstack.push(NavTarget.Room(roomId, initialElement = RoomFlowNode.NavTarget.RoomDetails)) + } } roomListEntryPoint .nodeBuilder(this, buildContext) @@ -193,7 +200,7 @@ class LoggedInFlowNode @AssistedInject constructor( } } else { val nodeLifecycleCallbacks = plugins() - val inputs = RoomFlowNode.Inputs(room) + val inputs = RoomFlowNode.Inputs(room, initialElement = navTarget.initialElement) createNode(buildContext, plugins = listOf(inputs) + nodeLifecycleCallbacks) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index 2b33e37c7e..0493ae7db1 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -59,7 +59,7 @@ class RoomFlowNode @AssistedInject constructor( roomMembershipObserver: RoomMembershipObserver, ) : BackstackNode( backstack = BackStack( - initialElement = NavTarget.Messages, + initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialElement, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -73,6 +73,7 @@ class RoomFlowNode @AssistedInject constructor( data class Inputs( val room: MatrixRoom, + val initialElement: NavTarget = NavTarget.Messages, ) : NodeInputs private val inputs: Inputs = inputs() @@ -98,6 +99,7 @@ class RoomFlowNode @AssistedInject constructor( navigateUp() } .launchIn(lifecycleScope) + inputs() } private fun fetchRoomMembers() = lifecycleScope.launch { diff --git a/changelog.d/427.feature b/changelog.d/427.feature new file mode 100644 index 0000000000..5aad0dcec8 --- /dev/null +++ b/changelog.d/427.feature @@ -0,0 +1 @@ +Room list contextual menu diff --git a/features/leaveroom/api/build.gradle.kts b/features/leaveroom/api/build.gradle.kts new file mode 100644 index 0000000000..83ca28b39a --- /dev/null +++ b/features/leaveroom/api/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.ksp) +} + +android { + namespace = "io.element.android.features.leaveroom.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + implementation(projects.libraries.matrix.api) + ksp(libs.showkase.processor) +} diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt new file mode 100644 index 0000000000..d1a3369ac6 --- /dev/null +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.leaveroom.api + +import io.element.android.libraries.matrix.api.core.RoomId + +sealed interface LeaveRoomEvent { + data class ShowConfirmation(val roomId: RoomId) : LeaveRoomEvent + object HideConfirmation : LeaveRoomEvent + data class LeaveRoom(val roomId: RoomId) : LeaveRoomEvent + object HideError : LeaveRoomEvent +} diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomPresenter.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomPresenter.kt new file mode 100644 index 0000000000..dd1f83691e --- /dev/null +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomPresenter.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.leaveroom.api + +import androidx.compose.runtime.Composable +import io.element.android.libraries.architecture.Presenter + +interface LeaveRoomPresenter : Presenter { + @Composable + override fun present(): LeaveRoomState +} diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt new file mode 100644 index 0000000000..7cb9926677 --- /dev/null +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.leaveroom.api + +import io.element.android.libraries.matrix.api.core.RoomId + +data class LeaveRoomState( + val confirmation: Confirmation = Confirmation.Hidden, + val progress: Progress = Progress.Hidden, + val error: Error = Error.Hidden, + val eventSink: (LeaveRoomEvent) -> Unit = {}, +) { + sealed interface Confirmation { + object Hidden : Confirmation + data class Generic(val roomId: RoomId) : Confirmation + data class PrivateRoom(val roomId: RoomId) : Confirmation + data class LastUserInRoom(val roomId: RoomId) : Confirmation + } + + sealed interface Progress { + object Hidden : Progress + object Shown : Progress + } + + sealed interface Error { + object Hidden : Error + object Shown : Error + } +} diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt new file mode 100644 index 0000000000..e9b08bcd18 --- /dev/null +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.leaveroom.api + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.core.RoomId + +class LeaveRoomStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + LeaveRoomState( + confirmation = LeaveRoomState.Confirmation.Hidden, + progress = LeaveRoomState.Progress.Hidden, + error = LeaveRoomState.Error.Hidden, + ), + LeaveRoomState( + confirmation = LeaveRoomState.Confirmation.Generic(A_ROOM_ID), + progress = LeaveRoomState.Progress.Hidden, + error = LeaveRoomState.Error.Hidden, + ), + LeaveRoomState( + confirmation = LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID), + progress = LeaveRoomState.Progress.Hidden, + error = LeaveRoomState.Error.Hidden, + ), + LeaveRoomState( + confirmation = LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID), + progress = LeaveRoomState.Progress.Hidden, + error = LeaveRoomState.Error.Hidden, + ), + LeaveRoomState( + confirmation = LeaveRoomState.Confirmation.Hidden, + progress = LeaveRoomState.Progress.Shown, + error = LeaveRoomState.Error.Hidden, + ), + LeaveRoomState( + confirmation = LeaveRoomState.Confirmation.Hidden, + progress = LeaveRoomState.Progress.Hidden, + error = LeaveRoomState.Error.Shown, + ), + ) +} + +private val A_ROOM_ID = RoomId("!aRoomId:aDomain") diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt new file mode 100644 index 0000000000..d1746ca917 --- /dev/null +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.leaveroom.api + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.R +import io.element.android.libraries.ui.strings.R as StringR + +@Composable +fun LeaveRoomView( + state: LeaveRoomState +) { + LeaveRoomConfirmationDialog(state) + LeaveRoomProgressDialog(state) + LeaveRoomErrorDialog(state) +} + +@Composable +private fun LeaveRoomConfirmationDialog( + state: LeaveRoomState, +) { + when (state.confirmation) { + is LeaveRoomState.Confirmation.Hidden -> {} + is LeaveRoomState.Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog( + text = StringR.string.leave_room_alert_private_subtitle, + roomId = state.confirmation.roomId, + eventSink = state.eventSink, + ) + + is LeaveRoomState.Confirmation.LastUserInRoom -> LeaveRoomConfirmationDialog( + text = StringR.string.leave_room_alert_empty_subtitle, + roomId = state.confirmation.roomId, + eventSink = state.eventSink, + ) + + is LeaveRoomState.Confirmation.Generic -> LeaveRoomConfirmationDialog( + text = StringR.string.leave_room_alert_subtitle, + roomId = state.confirmation.roomId, + eventSink = state.eventSink, + ) + } +} + +@Composable +private fun LeaveRoomConfirmationDialog( + @StringRes text: Int, + roomId: RoomId, + eventSink: (LeaveRoomEvent) -> Unit, +) { + ConfirmationDialog( + content = stringResource(text), + submitText = stringResource(R.string.action_leave), + onSubmitClicked = { eventSink(LeaveRoomEvent.LeaveRoom(roomId)) }, + onDismiss = { eventSink(LeaveRoomEvent.HideConfirmation) }, + ) +} + +@Composable +private fun LeaveRoomProgressDialog( + state: LeaveRoomState, +) { + when (state.progress) { + is LeaveRoomState.Progress.Hidden -> {} + is LeaveRoomState.Progress.Shown -> ProgressDialog( + text = stringResource(StringR.string.common_leaving_room), + ) + } +} + +@Composable +private fun LeaveRoomErrorDialog( + state: LeaveRoomState, +) { + when (state.error) { + is LeaveRoomState.Error.Hidden -> {} + is LeaveRoomState.Error.Shown -> ErrorDialog( + content = stringResource(StringR.string.error_unknown), + onDismiss = { state.eventSink(LeaveRoomEvent.HideError) } + ) + } +} + +@Preview +@Composable +internal fun LeaveRoomViewLightPreview( + @PreviewParameter(LeaveRoomStateProvider::class) state: LeaveRoomState +) = ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +internal fun LeaveRoomViewDarkPreview( + @PreviewParameter(LeaveRoomStateProvider::class) state: LeaveRoomState +) = ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: LeaveRoomState) { + Box( + modifier = Modifier.size(300.dp, 300.dp), + propagateMinConstraints = true, + ) { + LeaveRoomView(state = state) + } +} diff --git a/features/leaveroom/fake/build.gradle.kts b/features/leaveroom/fake/build.gradle.kts new file mode 100644 index 0000000000..19a057d5ba --- /dev/null +++ b/features/leaveroom/fake/build.gradle.kts @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.features.leaveroom.fake" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + api(projects.features.leaveroom.api) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.coroutines.core) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) +} diff --git a/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFake.kt b/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFake.kt new file mode 100644 index 0000000000..28c12b54ba --- /dev/null +++ b/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFake.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.leaveroom.fake + +import androidx.compose.runtime.Composable +import io.element.android.features.leaveroom.api.LeaveRoomEvent +import io.element.android.features.leaveroom.api.LeaveRoomPresenter +import io.element.android.features.leaveroom.api.LeaveRoomState +import javax.inject.Inject + +class LeaveRoomPresenterFake @Inject constructor() : LeaveRoomPresenter { + + val events = mutableListOf() + + private fun handleEvent(event: LeaveRoomEvent) { + events += event + } + + private var state = LeaveRoomState(eventSink = ::handleEvent) + set(value) { + field = value.copy(eventSink = ::handleEvent) + } + + fun givenState(state: LeaveRoomState) { + this.state = state + } + + @Composable + override fun present(): LeaveRoomState = state +} diff --git a/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFakeModule.kt b/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFakeModule.kt new file mode 100644 index 0000000000..b20b88db1c --- /dev/null +++ b/features/leaveroom/fake/src/main/kotlin/io/element/android/features/leaveroom/fake/LeaveRoomPresenterFakeModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.leaveroom.fake + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.leaveroom.api.LeaveRoomPresenter +import io.element.android.libraries.di.SessionScope + +@Module +@ContributesTo(SessionScope::class) +interface LeaveRoomPresenterFakeModule { + @Binds + fun leaveRoomPresenter(leaveRoomPresenter: LeaveRoomPresenterFake): LeaveRoomPresenter +} diff --git a/features/leaveroom/impl/build.gradle.kts b/features/leaveroom/impl/build.gradle.kts new file mode 100644 index 0000000000..8d26ea9271 --- /dev/null +++ b/features/leaveroom/impl/build.gradle.kts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.features.leaveroom.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + api(projects.features.leaveroom.api) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.coroutines.core) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) +} diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt new file mode 100644 index 0000000000..4b34e70fb4 --- /dev/null +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.leaveroom.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.features.leaveroom.api.LeaveRoomEvent +import io.element.android.features.leaveroom.api.LeaveRoomPresenter +import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Generic +import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.LastUserInRoom +import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.PrivateRoom +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +class LeaveRoomPresenterImpl @Inject constructor( + private val client: MatrixClient, + private val roomMembershipObserver: RoomMembershipObserver, + private val dispatchers: CoroutineDispatchers, +) : LeaveRoomPresenter { + @Composable + override fun present(): LeaveRoomState { + val scope = rememberCoroutineScope() + val confirmation = remember { mutableStateOf(LeaveRoomState.Confirmation.Hidden) } + val progress = remember { mutableStateOf(LeaveRoomState.Progress.Hidden) } + val error = remember { mutableStateOf(LeaveRoomState.Error.Hidden) } + + return LeaveRoomState( + confirmation = confirmation.value, + progress = progress.value, + error = error.value, + ) { event -> + when (event) { + is LeaveRoomEvent.ShowConfirmation -> scope.launch(dispatchers.io) { + showLeaveRoomAlert( + matrixClient = client, + roomId = event.roomId, + confirmation = confirmation, + ) + } + + is LeaveRoomEvent.HideConfirmation -> confirmation.value = LeaveRoomState.Confirmation.Hidden + is LeaveRoomEvent.LeaveRoom -> scope.launch(dispatchers.io) { + client.leaveRoom( + roomId = event.roomId, + roomMembershipObserver = roomMembershipObserver, + confirmation = confirmation, + progress = progress, + error = error, + ) + } + + is LeaveRoomEvent.HideError -> error.value = LeaveRoomState.Error.Hidden + } + } + } +} + +private suspend fun showLeaveRoomAlert( + matrixClient: MatrixClient, + roomId: RoomId, + confirmation: MutableState, +) { + matrixClient.getRoom(roomId)?.use { room -> + confirmation.value = when { + !room.isPublic -> PrivateRoom(roomId) + (room.memberCount() as? Async.Success)?.state == 1 -> LastUserInRoom(roomId) + else -> Generic(roomId) + } + } +} + +private suspend fun MatrixClient.leaveRoom( + roomId: RoomId, + roomMembershipObserver: RoomMembershipObserver, + confirmation: MutableState, + progress: MutableState, + error: MutableState, +) { + confirmation.value = LeaveRoomState.Confirmation.Hidden + progress.value = LeaveRoomState.Progress.Shown + getRoom(roomId)?.use { room -> + room.leave().onSuccess { + roomMembershipObserver.notifyUserLeftRoom(room.roomId) + }.onFailure { + Timber.e(it, "Error while leaving room ${room.name} - ${room.roomId}") + error.value = LeaveRoomState.Error.Shown + } + } + progress.value = LeaveRoomState.Progress.Hidden +} + +private suspend fun MatrixRoom.memberCount(): Async = membersStateFlow.first().let { membersState -> + when (membersState) { + MatrixRoomMembersState.Unknown -> Async.Uninitialized + is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size) + is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size) + is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.count { it.membership == RoomMembershipState.JOIN }) + } +} diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplModule.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplModule.kt new file mode 100644 index 0000000000..65403adb60 --- /dev/null +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.leaveroom.impl + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.leaveroom.api.LeaveRoomPresenter +import io.element.android.libraries.di.SessionScope + +@Module +@ContributesTo(SessionScope::class) +interface LeaveRoomPresenterImplModule { + @Binds + fun leaveRoomPresenter(leaveRoomPresenter: LeaveRoomPresenterImpl): LeaveRoomPresenter +} diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt new file mode 100644 index 0000000000..6dfc23be3f --- /dev/null +++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.leaveroom.impl + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.leaveroom.api.LeaveRoomEvent +import io.element.android.features.leaveroom.api.LeaveRoomPresenter +import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.test.runTest +import org.junit.Ignore +import org.junit.Test + +class LeaveRoomPresenterImplTest { + + @Test + fun `present - initial state hides all dialogs`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Hidden) + assertThat(initialState.progress).isEqualTo(LeaveRoomState.Progress.Hidden) + assertThat(initialState.error).isEqualTo(LeaveRoomState.Error.Hidden) + } + } + + @Test + fun `present - show generic confirmation`() = runTest { + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom() + ) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) + val confirmationState = awaitItem() + assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Generic(A_ROOM_ID)) + } + } + + @Test + fun `present - show private room confirmation`() = runTest { + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom(isPublic = false), + ) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) + val confirmationState = awaitItem() + assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID)) + } + } + + @Test + fun `present - show last user in room confirmation`() = runTest { + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom().apply { + givenRoomMembersState( + MatrixRoomMembersState.Ready( + listOf( + RoomMember( + userId = UserId(value = "@aUserId:aDomain"), + displayName = null, + avatarUrl = null, + membership = RoomMembershipState.JOIN, + isNameAmbiguous = false, + powerLevel = 0, + normalizedPowerLevel = 0, + isIgnored = false + ) + ) + ) + ) + }, + ) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) + val confirmationState = awaitItem() + assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID)) + } + } + + @Test + fun `present - leaving a room leaves the room`() = runTest { + val roomMembershipObserver = RoomMembershipObserver() + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom(), + ) + }, + roomMembershipObserver = roomMembershipObserver + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + cancelAndIgnoreRemainingEvents() + } + + // Membership observer should receive a 'left room' change + roomMembershipObserver.updates.take(1) + .onEach { update -> assertThat(update.change).isEqualTo(MembershipChange.LEFT) } + .collect() + } + + @Test + fun `present - show error if leave room fails`() = runTest { + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom().apply { + givenLeaveRoomError(RuntimeException("Blimey!")) + }, + ) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + val errorState = awaitItem() + assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown) + } + } + + @Test + @Ignore("TODO(Test the hiding/showing of the progress indicator too)") + fun `present - show progress indicator while leaving a room`() = runTest { + val roomMembershipObserver = RoomMembershipObserver() + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom(), + ) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + val progressState = awaitItem() + assertThat(progressState.progress).isEqualTo(LeaveRoomState.Progress.Shown) + val finalState = awaitItem() + assertThat(finalState.progress).isEqualTo(LeaveRoomState.Progress.Hidden) + } + + // Membership observer should receive a 'left room' change + roomMembershipObserver.updates.take(1) + .onEach { update -> assertThat(update.change).isEqualTo(MembershipChange.LEFT) } + .collect() + } + + @Test + fun `present - hide error hides the error`() = runTest { + val presenter = createPresenter( + client = FakeMatrixClient().apply { + givenGetRoomResult( + roomId = A_ROOM_ID, + result = FakeMatrixRoom().apply { + givenLeaveRoomError(RuntimeException("Blimey!")) + }, + ) + } + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + val errorState = awaitItem() + assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown) + errorState.eventSink(LeaveRoomEvent.HideError) + val hiddenErrorState = awaitItem() + assertThat(hiddenErrorState.error).isEqualTo(LeaveRoomState.Error.Hidden) + } + } +} + +private fun createPresenter( + client: MatrixClient = FakeMatrixClient(), + roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), + dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), +): LeaveRoomPresenter = LeaveRoomPresenterImpl( + client = client, + roomMembershipObserver = roomMembershipObserver, + dispatchers = dispatchers, +) diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 0470fc2e65..bcd618eccd 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { api(projects.libraries.usersearch.api) api(projects.services.apperror.api) implementation(libs.coil.compose) + implementation(projects.features.leaveroom.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) @@ -54,6 +55,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.usersearch.test) testImplementation(projects.tests.testutils) + testImplementation(projects.features.leaveroom.fake) ksp(libs.showkase.processor) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt index 3ef87d17e0..b7bb31757e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt @@ -17,7 +17,5 @@ package io.element.android.features.roomdetails.impl sealed interface RoomDetailsEvent { - data class LeaveRoom(val needsConfirmation: Boolean) : RoomDetailsEvent - object ClearLeaveRoomWarning : RoomDetailsEvent - object ClearError : RoomDetailsEvent + object LeaveRoom : RoomDetailsEvent } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 01562841ea..4e7d88b18e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -18,44 +18,33 @@ package io.element.android.features.roomdetails.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.features.leaveroom.api.LeaveRoomEvent +import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.ui.room.getDirectRoomMember -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import javax.inject.Inject class RoomDetailsPresenter @Inject constructor( private val room: MatrixRoom, - private val roomMembershipObserver: RoomMembershipObserver, - private val coroutineDispatchers: CoroutineDispatchers, private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory, + private val leaveRoomPresenter: LeaveRoomPresenter, ) : Presenter { @Composable override fun present(): RoomDetailsState { - val coroutineScope = rememberCoroutineScope() - val leaveRoomWarning = remember { - mutableStateOf(null) - } - val error = remember { - mutableStateOf(null) - } + val leaveRoomState = leaveRoomPresenter.present() LaunchedEffect(Unit) { room.updateMembers() } @@ -69,16 +58,8 @@ class RoomDetailsPresenter @Inject constructor( fun handleEvents(event: RoomDetailsEvent) { when (event) { - is RoomDetailsEvent.LeaveRoom -> { - coroutineScope.leaveRoom( - needsConfirmation = event.needsConfirmation, - memberCount = memberCount, - leaveRoomWarning = leaveRoomWarning, - error = error, - ) - } - RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning.value = null - RoomDetailsEvent.ClearError -> error.value = null + is RoomDetailsEvent.LeaveRoom -> + leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(room.roomId)) } } @@ -93,10 +74,9 @@ class RoomDetailsPresenter @Inject constructor( memberCount = memberCount, isEncrypted = room.isEncrypted, canInvite = canInvite, - displayLeaveRoomWarning = leaveRoomWarning.value, - error = error.value, roomType = roomType.value, roomMemberDetailsState = roomMemberDetailsState, + leaveRoomState = leaveRoomState, eventSink = ::handleEvents, ) } @@ -141,27 +121,4 @@ class RoomDetailsPresenter @Inject constructor( } } } - - private fun CoroutineScope.leaveRoom( - needsConfirmation: Boolean, - memberCount: Async, - leaveRoomWarning: MutableState, - error: MutableState, - ) = launch(coroutineDispatchers.io) { - if (needsConfirmation) { - leaveRoomWarning.value = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount) - } else { - room.leave() - .onSuccess { - roomMembershipObserver.notifyUserLeftRoom(room.roomId) - }.onFailure { - error.value = RoomDetailsError.AlertGeneric - } - leaveRoomWarning.value = null - } - } } - - - - diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index 90fa48c575..a046548b19 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -16,6 +16,7 @@ package io.element.android.features.roomdetails.impl +import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.room.RoomMember @@ -28,11 +29,10 @@ data class RoomDetailsState( val roomTopic: String?, val memberCount: Async, val isEncrypted: Boolean, - val displayLeaveRoomWarning: LeaveRoomWarning?, - val error: RoomDetailsError?, val roomType: RoomDetailsType, val roomMemberDetailsState: RoomMemberDetailsState?, val canInvite: Boolean, + val leaveRoomState: LeaveRoomState, val eventSink: (RoomDetailsEvent) -> Unit ) @@ -40,23 +40,3 @@ sealed interface RoomDetailsType { object Room : RoomDetailsType data class Dm(val roomMember: RoomMember) : RoomDetailsType } - -sealed class LeaveRoomWarning { - object Generic : LeaveRoomWarning() - object PrivateRoom : LeaveRoomWarning() - object LastUserInRoom : LeaveRoomWarning() - - companion object { - fun computeLeaveRoomWarning(isPublic: Boolean, memberCount: Async): LeaveRoomWarning { - return when { - !isPublic -> PrivateRoom - (memberCount as? Async.Success)?.state == 1 -> LastUserInRoom - else -> Generic - } - } - } -} - -sealed interface RoomDetailsError { - object AlertGeneric : RoomDetailsError -} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index 08da243487..9cb5b925fa 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.roomdetails.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.UserId @@ -70,11 +71,10 @@ fun aRoomDetailsState() = RoomDetailsState( "|| MAI iki/Marketing...", memberCount = Async.Success(32), isEncrypted = true, - displayLeaveRoomWarning = null, - error = null, canInvite = false, roomType = RoomDetailsType.Room, roomMemberDetailsState = null, + leaveRoomState = LeaveRoomState(), eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 293f290112..7d90342f65 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection +import io.element.android.features.leaveroom.api.LeaveRoomView import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection import io.element.android.libraries.architecture.isLoading @@ -56,8 +57,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.MainActionButton -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog -import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -68,7 +67,6 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable @@ -97,6 +95,8 @@ fun RoomDetailsView( .consumeWindowInsets(padding) .verticalScroll(rememberScrollState()) ) { + LeaveRoomView(state = state.leaveRoomState) + when (state.roomType) { RoomDetailsType.Room -> { RoomHeaderSection( @@ -145,23 +145,8 @@ fun RoomDetailsView( } OtherActionsSection(onLeaveRoom = { - state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) + state.eventSink(RoomDetailsEvent.LeaveRoom) }) - - if (state.displayLeaveRoomWarning != null) { - ConfirmLeaveRoomDialog( - leaveRoomWarning = state.displayLeaveRoomWarning, - onConfirmLeave = { state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) }, - onDismiss = { state.eventSink(RoomDetailsEvent.ClearLeaveRoomWarning) } - ) - } - - if (state.error != null) { - ErrorDialog( - content = stringResource(StringR.string.error_unknown), - onDismiss = { state.eventSink(RoomDetailsEvent.ClearError) } - ) - } } } } @@ -260,27 +245,6 @@ internal fun OtherActionsSection(onLeaveRoom: () -> Unit, modifier: Modifier = M } } -@Composable -internal fun ConfirmLeaveRoomDialog( - leaveRoomWarning: LeaveRoomWarning, - onConfirmLeave: () -> Unit, - onDismiss: () -> Unit -) { - val content = stringResource( - when (leaveRoomWarning) { - LeaveRoomWarning.PrivateRoom -> StringR.string.leave_room_alert_private_subtitle - LeaveRoomWarning.LastUserInRoom -> StringR.string.leave_room_alert_empty_subtitle - LeaveRoomWarning.Generic -> StringR.string.leave_room_alert_subtitle - } - ) - ConfirmationDialog( - content = content, - submitText = stringResource(StringR.string.action_leave), - onSubmitClicked = onConfirmLeave, - onDismiss = onDismiss, - ) -} - @LargeHeightPreview @Composable fun RoomDetailsLightPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) = diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 8e97c42936..0c950071ca 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -20,8 +20,7 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth -import io.element.android.features.roomdetails.impl.LeaveRoomWarning -import io.element.android.features.roomdetails.impl.RoomDetailsEvent +import io.element.android.features.leaveroom.fake.LeaveRoomPresenterFake import io.element.android.features.roomdetails.impl.RoomDetailsPresenter import io.element.android.features.roomdetails.impl.RoomDetailsType import io.element.android.features.roomdetails.impl.members.aRoomMember @@ -34,7 +33,6 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipState -import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -44,9 +42,6 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.take import kotlinx.coroutines.test.runTest import org.junit.Test @@ -62,7 +57,7 @@ class RoomDetailsPresenterTests { return RoomMemberDetailsPresenter(aMatrixClient(), room, roomMemberId) } } - return RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers, roomMemberDetailsPresenterFactory) + return RoomDetailsPresenter(room, roomMemberDetailsPresenterFactory, LeaveRoomPresenterFake()) } @Test @@ -155,103 +150,6 @@ class RoomDetailsPresenterTests { } } - @Test - fun `present - Leave with confirmation on private room shows a specific warning`() = runTest { - val room = aMatrixRoom(isPublic = false).apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) - } - val presenter = aRoomDetailsPresenter(room) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - skipItems(1) - - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) - val confirmationState = awaitItem() - Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.PrivateRoom) - } - } - - @Test - fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest { - val room = aMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(aRoomMember()))) - } - val presenter = aRoomDetailsPresenter(room) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - skipItems(1) - - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) - val confirmationState = awaitItem() - Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.LastUserInRoom) - } - } - - @Test - fun `present - Leave with confirmation shows a generic warning`() = runTest { - val room = aMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) - } - val presenter = aRoomDetailsPresenter(room) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - skipItems(1) - - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) - val confirmationState = awaitItem() - Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.Generic) - } - } - - @Test - fun `present - Leave without confirmation leaves the room`() = runTest { - val room = aMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) - } - val presenter = aRoomDetailsPresenter(room) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - skipItems(1) - - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) - - cancelAndIgnoreRemainingEvents() - } - - // Membership observer should receive a 'left room' change - roomMembershipObserver.updates.take(1) - .onEach { update -> Truth.assertThat(update.change).isEqualTo(MembershipChange.LEFT) } - .collect() - } - - @Test - fun `present - ClearError removes any error present`() = runTest { - val room = aMatrixRoom().apply { - givenLeaveRoomError(Throwable()) - } - val presenter = aRoomDetailsPresenter(room) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - skipItems(1) - - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) - val errorState = awaitItem() - Truth.assertThat(errorState.error).isNotNull() - errorState.eventSink(RoomDetailsEvent.ClearError) - Truth.assertThat(awaitItem().error).isNull() - } - } - @Test fun `present - initial state when user can invite others to room`() = runTest { val room = aMatrixRoom().apply { diff --git a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt index dd4347ba0c..eeefa15c02 100644 --- a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt +++ b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt @@ -36,6 +36,7 @@ interface RoomListEntryPoint : FeatureEntryPoint { fun onSettingsClicked() fun onSessionVerificationClicked() fun onInvitesClicked() + fun onRoomSettingsClicked(roomId: RoomId) } } diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index 9150ff4c95..100ab15437 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(projects.libraries.dateformatter.api) implementation(projects.features.invitelist.api) implementation(projects.features.networkmonitor.api) + implementation(projects.features.leaveroom.api) implementation(libs.accompanist.placeholder) api(projects.features.roomlist.api) ksp(libs.showkase.processor) @@ -66,6 +67,7 @@ dependencies { testImplementation(projects.features.invitelist.test) testImplementation(projects.features.networkmonitor.test) testImplementation(projects.tests.testutils) + testImplementation(projects.features.leaveroom.fake) androidTestImplementation(libs.test.junitext) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt new file mode 100644 index 0000000000..4e172bbbfe --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.VectorIcons +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.R as StringR + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomListContextMenu( + contextMenu: RoomListState.ContextMenu.Shown, + eventSink: (RoomListEvents) -> Unit, + onRoomSettingsClicked: (roomId: RoomId) -> Unit, +) { + ModalBottomSheet( + onDismissRequest = { eventSink(RoomListEvents.HideContextMenu) }, + ) { + RoomListModalBottomSheetContent( + contextMenu = contextMenu, + onRoomSettingsClicked = { + eventSink(RoomListEvents.HideContextMenu) + onRoomSettingsClicked(it) + }, + onLeaveRoomClicked = { + eventSink(RoomListEvents.HideContextMenu) + eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId)) + } + ) + } +} + +@Composable +private fun RoomListModalBottomSheetContent( + contextMenu: RoomListState.ContextMenu.Shown, + onRoomSettingsClicked: (roomId: RoomId) -> Unit, + onLeaveRoomClicked: (roomId: RoomId) -> Unit, +) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + ListItem( + headlineContent = { + Text( + text = contextMenu.roomName, + fontWeight = FontWeight.Bold, + ) + } + ) + ListItem( + headlineContent = { + Text(text = stringResource(id = StringR.string.common_settings)) + }, + modifier = Modifier.clickable { onRoomSettingsClicked(contextMenu.roomId) }, + leadingContent = { + Icon( + imageVector = Icons.Outlined.Settings, + contentDescription = stringResource(id = StringR.string.common_settings), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + ) + ListItem( + headlineContent = { + Text( + text = stringResource(id = StringR.string.action_leave_room), + color = ElementTheme.colors.textActionCritical, + ) + }, + modifier = Modifier.clickable { onLeaveRoomClicked(contextMenu.roomId) }, + leadingContent = { + Icon( + resourceId = VectorIcons.DoorOpen, + contentDescription = stringResource(id = StringR.string.action_leave_room), + modifier = Modifier.size(20.dp), + tint = ElementTheme.colors.textActionCritical, + ) + } + ) + Spacer(modifier = Modifier.height(32.dp)) + } +} + +// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up. +// see: https://issuetracker.google.com/issues/283843380 +// Remove this preview when the issue is fixed. +@Preview +@Composable +internal fun RoomListModalBottomSheetContentLightPreview() = + ElementPreviewLight { ContentToPreview() } + +// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up. +// see: https://issuetracker.google.com/issues/283843380 +// Remove this preview when the issue is fixed. +@Preview +@Composable +internal fun RoomListModalBottomSheetContentDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + RoomListModalBottomSheetContent( + contextMenu = RoomListState.ContextMenu.Shown( + roomId = RoomId(value = "!aRoom:aDomain"), + roomName = "aRoom" + ), + onRoomSettingsClicked = {}, + onLeaveRoomClicked = {} + ) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt index 684342bee8..e95b5bd60d 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt @@ -16,9 +16,15 @@ package io.element.android.features.roomlist.impl +import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.libraries.matrix.api.core.RoomId + sealed interface RoomListEvents { data class UpdateFilter(val newFilter: String) : RoomListEvents data class UpdateVisibleRange(val range: IntRange) : RoomListEvents object DismissRequestVerificationPrompt : RoomListEvents object ToggleSearchResults : RoomListEvents + data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents + object HideContextMenu : RoomListEvents + data class LeaveRoom(val roomId: RoomId) : RoomListEvents } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt index 7641f015cc..50a7a7bfbe 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt @@ -56,17 +56,22 @@ class RoomListNode @AssistedInject constructor( plugins().forEach { it.onInvitesClicked() } } + private fun onRoomSettingsClicked(roomId: RoomId) { + plugins().forEach { it.onRoomSettingsClicked(roomId) } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() RoomListView( state = state, - modifier = modifier, onRoomClicked = this::onRoomClicked, - onOpenSettings = this::onOpenSettings, + onSettingsClicked = this::onOpenSettings, onCreateRoomClicked = this::onCreateRoomClicked, onVerifyClicked = this::onSessionVerificationClicked, onInvitesClicked = this::onInvitesClicked, + onRoomSettingsClicked = this::onRoomSettingsClicked, + modifier = modifier, ) } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 59faa5afbe..e1f37ae5e6 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -28,6 +28,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.features.leaveroom.api.LeaveRoomEvent +import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders import io.element.android.libraries.architecture.Presenter @@ -62,10 +64,12 @@ class RoomListPresenter @Inject constructor( private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, private val inviteStateDataSource: InviteStateDataSource, + private val leaveRoomPresenter: LeaveRoomPresenter, ) : Presenter { @Composable override fun present(): RoomListState { + val leaveRoomState = leaveRoomPresenter.present() val matrixUser: MutableState = rememberSaveable { mutableStateOf(null) } @@ -97,6 +101,8 @@ class RoomListPresenter @Inject constructor( var displaySearchResults by rememberSaveable { mutableStateOf(false) } + var contextMenu by remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } + fun handleEvents(event: RoomListEvents) { when (event) { is RoomListEvents.UpdateFilter -> filter = event.newFilter @@ -108,6 +114,14 @@ class RoomListPresenter @Inject constructor( } displaySearchResults = !displaySearchResults } + is RoomListEvents.ShowContextMenu -> { + contextMenu = RoomListState.ContextMenu.Shown( + roomId = event.roomListRoomSummary.roomId, + roomName = event.roomListRoomSummary.name + ) + } + is RoomListEvents.HideContextMenu -> contextMenu = RoomListState.ContextMenu.Hidden + is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId)) } } @@ -132,6 +146,8 @@ class RoomListPresenter @Inject constructor( hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, invitesState = inviteStateDataSource.inviteState(), displaySearchResults = displaySearchResults, + contextMenu = contextMenu, + leaveRoomState = leaveRoomState, eventSink = ::handleEvents ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index 0f8de1046d..7905b5bc61 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -17,8 +17,10 @@ package io.element.android.features.roomlist.impl import androidx.compose.runtime.Immutable +import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList @@ -33,8 +35,18 @@ data class RoomListState( val snackbarMessage: SnackbarMessage?, val invitesState: InvitesState, val displaySearchResults: Boolean, - val eventSink: (RoomListEvents) -> Unit -) + val contextMenu: ContextMenu, + val leaveRoomState: LeaveRoomState, + val eventSink: (RoomListEvents) -> Unit, +) { + sealed interface ContextMenu { + object Hidden : ContextMenu + data class Shown( + val roomId: RoomId, + val roomName: String, + ) : ContextMenu + } +} enum class InvitesState { NoInvites, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 8bd6537d52..c5598b5426 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.roomlist.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -39,6 +40,9 @@ open class RoomListStateProvider : PreviewParameterProvider { aRoomListState().copy(invitesState = InvitesState.NewInvites), aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()), aRoomListState().copy(displaySearchResults = true), + aRoomListState().copy(contextMenu = RoomListState.ContextMenu.Shown( + roomId = RoomId("!aRoom:aDomain"), roomName = "A nice room name" + )) ) } @@ -52,6 +56,8 @@ internal fun aRoomListState() = RoomListState( displayVerificationPrompt = false, invitesState = InvitesState.NoInvites, displaySearchResults = false, + contextMenu = RoomListState.ContextMenu.Hidden, + leaveRoomState = LeaveRoomState(), eventSink = {} ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index dc3c7bff7a..49b2c3604c 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -16,7 +16,6 @@ package io.element.android.features.roomlist.impl -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -62,6 +61,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp +import io.element.android.features.leaveroom.api.LeaveRoomView import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import io.element.android.features.roomlist.impl.components.RoomListTopBar import io.element.android.features.roomlist.impl.components.RoomSummaryRow @@ -88,20 +88,38 @@ import io.element.android.libraries.ui.strings.R as StringR @Composable fun RoomListView( state: RoomListState, + onRoomClicked: (RoomId) -> Unit, + onSettingsClicked: () -> Unit, + onVerifyClicked: () -> Unit, + onCreateRoomClicked: () -> Unit, + onInvitesClicked: () -> Unit, + onRoomSettingsClicked: (roomId: RoomId) -> Unit, modifier: Modifier = Modifier, - onRoomClicked: (RoomId) -> Unit = {}, - onOpenSettings: () -> Unit = {}, - onVerifyClicked: () -> Unit = {}, - onCreateRoomClicked: () -> Unit = {}, - onInvitesClicked: () -> Unit = {}, ) { Column(modifier = modifier) { ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) Box { + fun onRoomLongClicked( + roomListRoomSummary: RoomListRoomSummary + ) { + state.eventSink(RoomListEvents.ShowContextMenu(roomListRoomSummary)) + } + + if (state.contextMenu is RoomListState.ContextMenu.Shown) { + RoomListContextMenu( + contextMenu = state.contextMenu, + eventSink = state.eventSink, + onRoomSettingsClicked = onRoomSettingsClicked, + ) + } + + LeaveRoomView(state = state.leaveRoomState) + RoomListContent( state = state, onRoomClicked = onRoomClicked, - onOpenSettings = onOpenSettings, + onRoomLongClicked = { onRoomLongClicked(it) }, + onOpenSettings = onSettingsClicked, onVerifyClicked = onVerifyClicked, onCreateRoomClicked = onCreateRoomClicked, onInvitesClicked = onInvitesClicked, @@ -110,6 +128,7 @@ fun RoomListView( RoomListSearchResultView( state = state, onRoomClicked = onRoomClicked, + onRoomLongClicked = { onRoomLongClicked(it) }, modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) @@ -125,12 +144,12 @@ fun RoomListContent( modifier: Modifier = Modifier, onVerifyClicked: () -> Unit = {}, onRoomClicked: (RoomId) -> Unit = {}, + onRoomLongClicked: (RoomListRoomSummary) -> Unit = {}, onOpenSettings: () -> Unit = {}, onCreateRoomClicked: () -> Unit = {}, onInvitesClicked: () -> Unit = {}, ) { fun onRoomClicked(room: RoomListRoomSummary) { - if (room.roomId == null) return onRoomClicked(room.roomId) } @@ -237,7 +256,11 @@ fun RoomListContent( items = state.roomList, contentType = { room -> room.contentType() }, ) { room -> - RoomSummaryRow(room = room, onClick = ::onRoomClicked) + RoomSummaryRow( + room = room, + onClick = ::onRoomClicked, + onLongClick = onRoomLongClicked, + ) } } } @@ -339,13 +362,25 @@ internal fun RoomListViewDarkPreview(@PreviewParameter(RoomListStateProvider::cl @Composable private fun ContentToPreview(state: RoomListState) { - RoomListView(state) + RoomListView( + state = state, + onRoomClicked = {}, + onSettingsClicked = {}, + onVerifyClicked = {}, + onCreateRoomClicked = {}, + onInvitesClicked = {}, + onRoomSettingsClicked = {} + ) } @Preview @Composable internal fun RoomListSearchResultContentPreview() { ElementPreviewLight { - RoomListSearchResultContent(state = aRoomListState(), onRoomClicked = {}) + RoomListSearchResultContent( + state = aRoomListState(), + onRoomClicked = {}, + onRoomLongClicked = {} + ) } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt index 0709b41ef2..67847977e6 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt @@ -16,8 +16,9 @@ package io.element.android.features.roomlist.impl.components +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -70,17 +71,20 @@ import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator private val minHeight = 72.dp +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun RoomSummaryRow( room: RoomListRoomSummary, + onClick: (RoomListRoomSummary) -> Unit, + onLongClick: (RoomListRoomSummary) -> Unit, modifier: Modifier = Modifier, - onClick: (RoomListRoomSummary) -> Unit = {}, ) { val clickModifier = if (room.isPlaceholder) { modifier } else { - modifier.clickable( + modifier.combinedClickable( onClick = { onClick(room) }, + onLongClick = { onLongClick(room) }, indication = rememberRipple(), interactionSource = remember { MutableInteractionSource() } ) @@ -214,5 +218,9 @@ internal fun RoomSummaryRowDarkPreview(@PreviewParameter(RoomListRoomSummaryProv @Composable private fun ContentToPreview(data: RoomListRoomSummary) { - RoomSummaryRow(data) + RoomSummaryRow( + room = data, + onClick = {}, + onLongClick = {} + ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt index d938cce04f..734d1ce9e1 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt @@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.core.RoomId @Immutable data class RoomListRoomSummary( val id: String, - val roomId: RoomId?, + val roomId: RoomId, val name: String = "", val hasUnread: Boolean = false, val timestamp: String? = null, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryPlaceholders.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryPlaceholders.kt index d6de1544cb..47e3a8ca28 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryPlaceholders.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryPlaceholders.kt @@ -17,13 +17,14 @@ package io.element.android.features.roomlist.impl.model import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.matrix.api.core.RoomId object RoomListRoomSummaryPlaceholders { fun create(id: String): RoomListRoomSummary { return RoomListRoomSummary( id = id, - roomId = null, + roomId = RoomId("!aRoom:domain"), isPlaceholder = true, name = "Short name", timestamp = "hh:mm", diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt index bdd4858ce3..f70c46c18b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt @@ -70,6 +70,7 @@ import io.element.android.libraries.ui.strings.R internal fun RoomListSearchResultView( state: RoomListState, onRoomClicked: (RoomId) -> Unit, + onRoomLongClicked: (RoomListRoomSummary) -> Unit, modifier: Modifier = Modifier, ) { AnimatedVisibility( @@ -85,7 +86,11 @@ internal fun RoomListSearchResultView( }) ) { if (state.displaySearchResults) { - RoomListSearchResultContent(state = state, onRoomClicked = onRoomClicked) + RoomListSearchResultContent( + state = state, + onRoomClicked = onRoomClicked, + onRoomLongClicked = onRoomLongClicked, + ) } } } @@ -96,6 +101,7 @@ internal fun RoomListSearchResultView( internal fun RoomListSearchResultContent( state: RoomListState, onRoomClicked: (RoomId) -> Unit, + onRoomLongClicked: (RoomListRoomSummary) -> Unit, modifier: Modifier = Modifier, ) { val borderColor = MaterialTheme.colorScheme.tertiary @@ -104,7 +110,6 @@ internal fun RoomListSearchResultContent( state.eventSink(RoomListEvents.ToggleSearchResults) } fun onRoomClicked(room: RoomListRoomSummary) { - if (room.roomId == null) return onRoomClicked(room.roomId) } Scaffold( @@ -197,7 +202,11 @@ internal fun RoomListSearchResultContent( items = state.filteredRoomList, contentType = { room -> room.contentType() }, ) { room -> - RoomSummaryRow(room = room, onClick = ::onRoomClicked) + RoomSummaryRow( + room = room, + onClick = ::onRoomClicked, + onLongClick = onRoomLongClicked, + ) } } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index 104c419f74..72996727dd 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -20,8 +20,11 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth +import io.element.android.features.leaveroom.api.LeaveRoomEvent +import io.element.android.features.leaveroom.fake.LeaveRoomPresenterFake import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -54,6 +57,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -82,6 +86,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -103,6 +108,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -132,6 +138,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -164,6 +171,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -202,6 +210,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -253,6 +262,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -278,6 +288,7 @@ class RoomListPresenterTests { FakeNetworkMonitor(), SnackbarDispatcher(), FakeInviteDataSource(inviteStateFlow), + LeaveRoomPresenterFake(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -297,6 +308,89 @@ class RoomListPresenterTests { } } + @Test + fun `present - show context menu`() = runTest { + val presenter = RoomListPresenter( + FakeMatrixClient(A_SESSION_ID), + createDateFormatter(), + FakeRoomLastMessageFormatter(), + FakeSessionVerificationService(), + FakeNetworkMonitor(), + SnackbarDispatcher(), + FakeInviteDataSource(), + LeaveRoomPresenterFake(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + + val initialState = awaitItem() + val summary = aRoomListRoomSummary() + initialState.eventSink(RoomListEvents.ShowContextMenu(summary)) + + val shownState = awaitItem() + Truth.assertThat(shownState.contextMenu) + .isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name)) + } + } + + @Test + fun `present - hide context menu`() = runTest { + val presenter = RoomListPresenter( + FakeMatrixClient(A_SESSION_ID), + createDateFormatter(), + FakeRoomLastMessageFormatter(), + FakeSessionVerificationService(), + FakeNetworkMonitor(), + SnackbarDispatcher(), + FakeInviteDataSource(), + LeaveRoomPresenterFake(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + + val initialState = awaitItem() + val summary = aRoomListRoomSummary() + initialState.eventSink(RoomListEvents.ShowContextMenu(summary)) + + val shownState = awaitItem() + Truth.assertThat(shownState.contextMenu) + .isEqualTo(RoomListState.ContextMenu.Shown(summary.roomId, summary.name)) + shownState.eventSink(RoomListEvents.HideContextMenu) + + val hiddenState = awaitItem() + Truth.assertThat(hiddenState.contextMenu).isEqualTo(RoomListState.ContextMenu.Hidden) + } + } + + @Test + fun `present - leave room calls into leave room presenter`() = runTest { + val leaveRoomPresenter = LeaveRoomPresenterFake() + val presenter = RoomListPresenter( + FakeMatrixClient(A_SESSION_ID), + createDateFormatter(), + FakeRoomLastMessageFormatter(), + FakeSessionVerificationService(), + FakeNetworkMonitor(), + SnackbarDispatcher(), + FakeInviteDataSource(), + leaveRoomPresenter, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + + val initialState = awaitItem() + initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID)) + + Truth.assertThat(leaveRoomPresenter.events).containsExactly(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) + } + } + private fun createDateFormatter(): LastMessageTimestampFormatter { return FakeLastMessageTimestampFormatter().apply { givenFormat(A_FORMATTED_DATE) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt index a11b0b96bb..c2a82f0e21 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt @@ -22,4 +22,5 @@ object VectorIcons { val Delete = R.drawable.ic_baseline_delete_outline_24 val Reply = R.drawable.ic_baseline_reply_24 val Edit = R.drawable.ic_baseline_edit_24 + val DoorOpen = R.drawable.ic_door_open_24 } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt new file mode 100644 index 0000000000..0c98caaa7d --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.contentColorFor +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.preview.PreviewGroup + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ModalBottomSheet( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + sheetState: SheetState = rememberModalBottomSheetState(), + shape: Shape = BottomSheetDefaults.ExpandedShape, + containerColor: Color = BottomSheetDefaults.ContainerColor, + contentColor: Color = contentColorFor(containerColor), + tonalElevation: Dp = BottomSheetDefaults.Elevation, + scrimColor: Color = BottomSheetDefaults.ScrimColor, + dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, + content: @Composable ColumnScope.() -> Unit, +) { + androidx.compose.material3.ModalBottomSheet( + onDismissRequest = onDismissRequest, + modifier = modifier, + sheetState = sheetState, + shape = shape, + containerColor = containerColor, + contentColor = contentColor, + tonalElevation = tonalElevation, + scrimColor = scrimColor, + dragHandle = dragHandle, + content = content, + ) +} + +// This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380 +@Preview(group = PreviewGroup.BottomSheets) +@Composable +internal fun ModalBottomSheetLightPreview() = + ElementPreviewLight { ContentToPreview() } + +// This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380 +@Preview(group = PreviewGroup.BottomSheets) +@Composable +internal fun ModalBottomSheetDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ContentToPreview() { + Box( + modifier = Modifier.fillMaxSize(), + ) { + ModalBottomSheet( + onDismissRequest = {}, + sheetState = SheetState( + skipPartiallyExpanded = true, + initialValue = SheetValue.Expanded, + skipHiddenState = true, + ), + ) { + Text( + text = "Sheet Content", + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 20.dp) + .background(color = Color.Green) + ) + } + } +} diff --git a/libraries/designsystem/src/main/res/drawable/ic_door_open_24.xml b/libraries/designsystem/src/main/res/drawable/ic_door_open_24.xml new file mode 100644 index 0000000000..7d2eec40f5 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_door_open_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 83e358ad48..73e09d2314 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(projects.libraries.dateformatter.impl) implementation(projects.features.invitelist.impl) implementation(projects.features.roomlist.impl) + implementation(projects.features.leaveroom.impl) implementation(projects.features.login.impl) implementation(projects.features.networkmonitor.impl) implementation(libs.coroutines.core) diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index aab7d5ca5f..9a5e1a8631 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import io.element.android.features.invitelist.impl.DefaultSeenInvitesStore +import io.element.android.features.leaveroom.impl.LeaveRoomPresenterImpl import io.element.android.features.networkmonitor.impl.NetworkMonitorImpl import io.element.android.features.roomlist.impl.DefaultInviteStateDataSource import io.element.android.features.roomlist.impl.DefaultRoomLastMessageFormatter @@ -33,6 +34,7 @@ import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.datetime.Clock @@ -57,7 +59,8 @@ class RoomListScreen( sessionVerificationService = sessionVerificationService, networkMonitor = NetworkMonitorImpl(context), snackbarDispatcher = SnackbarDispatcher(), - inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers) + inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers), + leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver() ,coroutineDispatchers) ) @Composable @@ -82,8 +85,13 @@ class RoomListScreen( val state = presenter.present() RoomListView( state = state, - modifier = modifier, onRoomClicked = ::onRoomClicked, + onSettingsClicked = {}, + onVerifyClicked = {}, + onCreateRoomClicked = {}, + onInvitesClicked = {}, + onRoomSettingsClicked = {}, + modifier = modifier, ) DisposableEffect(Unit) { diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..83e91d47af --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f20d59b9bb1ea540e7a1f0c1aeefd1439340625429af672bd6cd00f0c3fdc7f6 +size 5150 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4738df50ef --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03ceca5d30354b7736be1bc079053db5ff82d2ac330161c5e07a5d801a24220a +size 20341 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..31fb5c37a5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfe27aff64c8ec1c71b9c41ebd7cef9a59df69275bbb3637cf4a7e0c88bdcb0e +size 32997 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c8e9a36e71 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2148b3ffec6f6e953617b035702299f4bb1b5f7f56eacce907f333076da75bd9 +size 36701 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fa09501b15 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7de61d642b9b98c9f0bb8b015b5c57d9645a1552ebed6f0cd215c051c47f0896 +size 10593 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dbf334a4a6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59529b30fa2cfef483b365ab888e9327b2dcbae4e40a571642cb534d80379ce2 +size 18439 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2a715455c6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47e2b24a91c59154221e16b1c9e80689300c077d3f19b8dba4d539754c630e70 +size 5144 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ecbf7acd67 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e58c6e0ea9e0df37794d4ac2f8e40ff7995454626618fc0183b5900d62dcfec +size 20456 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..424bc9edae --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71ab2aab2b1aba8baa95c2bd844d5b01410bb3d6004f561038919cf6a7a6b6f5 +size 32991 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..af9816dd4e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ceb2bbf886fdd28c8ed2aa755241725de106ae998825e10d3c6608f100987cb1 +size 36673 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..34e8908056 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:165597506f493c5adedc840fc22ac0218517bb8ae1c4a7f1fabe43665d0f154f +size 10740 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..104e886089 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.leaveroom.api_null_DefaultGroup_LeaveRoomViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:470b5e47f6b21819aa751736f24f7afd88a5ce3ac911f9d0b6bfbdf94e2f918b +size 18597 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6ac7bc4bf1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:514d015e7edd0c48746439c9e6a929cd24f92c36302d0d746ca71f0482283439 +size 23123 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3da8979b19 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListModalBottomSheetContentLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78f9c2e01fd3d0ef827808265308555542e5d24055714c7a632e8baf1baffc8e +size 21974 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e4b5f95b8d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cdb131c68de1fce5a3319151e39148e9f3a71c7bc3984e89ec0a80abf0f7288 +size 37044 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2aa0933d88 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2a261b30866af95b856ee1e7d6ac2cbe2d638cf80277645f361b043d2f94e60 +size 36658 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_BottomSheets_ModalBottomSheetDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_BottomSheets_ModalBottomSheetDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fca921c50b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_BottomSheets_ModalBottomSheetDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1c1eedbab868e0c2501220293608572850e052f685f7076ec939b3f1a9abf27 +size 4464 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_BottomSheets_ModalBottomSheetLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_BottomSheets_ModalBottomSheetLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_BottomSheets_ModalBottomSheetLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 From fdb1ebf0cef9709aedc21a9e91b220a0679309cc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 May 2023 11:51:46 +0200 Subject: [PATCH 11/34] Sort by filename for easier search in the logs. --- plugins/src/main/kotlin/extension/DependencyHandleScope.kt | 3 ++- settings.gradle.kts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index c433ed2e07..1819124002 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -54,7 +54,7 @@ fun DependencyHandlerScope.composeDependencies(libs: LibrariesForLibs) { } private fun DependencyHandlerScope.addImplementationProjects(directory: File, path: String, nameFilter: String) { - directory.listFiles().orEmpty().forEach { file -> + directory.listFiles().orEmpty().also { it.sort() }.forEach { file -> if (file.isDirectory) { val newPath = "$path:${file.name}" val buildFile = File(file, "build.gradle.kts") @@ -104,6 +104,7 @@ fun DependencyHandlerScope.allFeaturesApi(rootDir: File) { val featuresDir = File(rootDir, "features") addImplementationProjects(featuresDir, ":features", "api") } + fun DependencyHandlerScope.allFeaturesImpl(rootDir: File) { val featuresDir = File(rootDir, "features") addImplementationProjects(featuresDir, ":features", "impl") diff --git a/settings.gradle.kts b/settings.gradle.kts index 1173288adb..0fae407502 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -56,7 +56,7 @@ include(":anvilcodegen") include(":samples:minimal") fun includeProjects(directory: File, path: String, maxDepth: Int = 1) { - directory.listFiles().orEmpty().forEach { file -> + directory.listFiles().orEmpty().also { it.sort() }.forEach { file -> if (file.isDirectory) { val newPath = "$path:${file.name}" val buildFile = File(file, "build.gradle.kts") From 80b656a9c5895e74f9fbfae7c6fce506d0ad830b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 May 2023 12:03:51 +0200 Subject: [PATCH 12/34] Use gradle logger to filter out when running with only warning logs. --- app/build.gradle.kts | 2 +- appnav/build.gradle.kts | 2 +- .../kotlin/extension/DependencyHandleScope.kt | 20 ++++++++++++------- settings.gradle.kts | 2 +- tests/uitests/build.gradle.kts | 2 +- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index da9a4ab3e9..1909d9fa0f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -206,7 +206,7 @@ knit { dependencies { allLibrariesImpl() allServicesImpl() - allFeaturesImpl(rootDir) + allFeaturesImpl(rootDir, logger) implementation(projects.libraries.deeplink) implementation(projects.tests.uitests) implementation(projects.anvilannotations) diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 5b1c4b1ced..2ed497567f 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -36,7 +36,7 @@ dependencies { implementation(libs.dagger) kapt(libs.dagger.compiler) - allFeaturesApi(rootDir) + allFeaturesApi(rootDir, logger) implementation(projects.libraries.core) implementation(projects.libraries.androidutils) diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 1819124002..595f30cf30 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -19,6 +19,7 @@ package extension import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.kotlin.dsl.DependencyHandlerScope import org.gradle.kotlin.dsl.project +import org.gradle.api.logging.Logger import java.io.File private fun DependencyHandlerScope.implementation(dependency: Any) = dependencies.add("implementation", dependency) @@ -53,16 +54,21 @@ fun DependencyHandlerScope.composeDependencies(libs: LibrariesForLibs) { implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5") } -private fun DependencyHandlerScope.addImplementationProjects(directory: File, path: String, nameFilter: String) { +private fun DependencyHandlerScope.addImplementationProjects( + directory: File, + path: String, + nameFilter: String, + logger: Logger, +) { directory.listFiles().orEmpty().also { it.sort() }.forEach { file -> if (file.isDirectory) { val newPath = "$path:${file.name}" val buildFile = File(file, "build.gradle.kts") if (buildFile.exists() && file.name == nameFilter) { implementation(project(newPath)) - println("Added implementation(project($newPath))") + logger.lifecycle("Added implementation(project($newPath))") } else { - addImplementationProjects(file, newPath, nameFilter) + addImplementationProjects(file, newPath, nameFilter, logger) } } } @@ -100,12 +106,12 @@ fun DependencyHandlerScope.allServicesImpl() { implementation(project(":services:toolbox:impl")) } -fun DependencyHandlerScope.allFeaturesApi(rootDir: File) { +fun DependencyHandlerScope.allFeaturesApi(rootDir: File, logger: Logger) { val featuresDir = File(rootDir, "features") - addImplementationProjects(featuresDir, ":features", "api") + addImplementationProjects(featuresDir, ":features", "api", logger) } -fun DependencyHandlerScope.allFeaturesImpl(rootDir: File) { +fun DependencyHandlerScope.allFeaturesImpl(rootDir: File, logger: Logger) { val featuresDir = File(rootDir, "features") - addImplementationProjects(featuresDir, ":features", "impl") + addImplementationProjects(featuresDir, ":features", "impl", logger) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0fae407502..9b2d507b7e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -62,7 +62,7 @@ fun includeProjects(directory: File, path: String, maxDepth: Int = 1) { val buildFile = File(file, "build.gradle.kts") if (buildFile.exists()) { include(newPath) - println("Included project: $newPath") + logger.lifecycle("Included project: $newPath") } else if (maxDepth > 0) { includeProjects(file, newPath, maxDepth - 1) } diff --git a/tests/uitests/build.gradle.kts b/tests/uitests/build.gradle.kts index e120e55087..307cbcdd5d 100644 --- a/tests/uitests/build.gradle.kts +++ b/tests/uitests/build.gradle.kts @@ -37,5 +37,5 @@ dependencies { implementation(libs.showkase) allLibrariesImpl() - allFeaturesImpl(rootDir) + allFeaturesImpl(rootDir, logger) } From 6edc7a9323a142bf280a5c6917ee2f5fe727ea31 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 May 2023 12:09:18 +0200 Subject: [PATCH 13/34] Reduce logging level to `--warn` to reduce verbosity for testing and checking quality workflows. Keep default (lifecycle) for build workflow. https://docs.gradle.org/current/userguide/logging.html --- .github/workflows/quality.yml | 2 +- .github/workflows/tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index e668c17275..86e823f335 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -9,7 +9,7 @@ on: # Enrich gradle.properties for CI/CD env: GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g" -Dkotlin.incremental=false - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon + CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon --warn jobs: check: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7b6e5eaf7d..aea05b5a22 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ on: # Enrich gradle.properties for CI/CD env: GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false - CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 + CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 --warn jobs: tests: From 9d66af834db415bb4d78ccba44f979cf2d3b1bdb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 16 May 2023 11:45:58 +0200 Subject: [PATCH 14/34] Move RoomLastMessageFormatter and related class and resource to the new module `eventformatter`, in order to prepare rendering of state event in the timeline. --- .../src/main/res/values-de/translations.xml | 3 +- .../src/main/res/values-de/translations.xml | 4 ++ .../src/main/res/values-de/translations.xml | 2 + features/roomlist/impl/build.gradle.kts | 2 + .../roomlist/impl/RoomListPresenter.kt | 1 + .../src/main/res/values-de/translations.xml | 36 ------------ .../src/main/res/values-es/translations.xml | 54 ------------------ .../src/main/res/values-it/translations.xml | 54 ------------------ .../src/main/res/values-ro/translations.xml | 54 ------------------ .../impl/src/main/res/values/localazy.xml | 54 ------------------ .../roomlist/impl/RoomListPresenterTests.kt | 1 + libraries/eventformatter/api/build.gradle.kts | 27 +++++++++ .../api}/RoomLastMessageFormatter.kt | 2 +- .../eventformatter/impl/build.gradle.kts | 49 ++++++++++++++++ .../impl/DefaultRoomLastMessageFormatter.kt | 3 +- .../src/main/res/values-de/translations.xml | 39 +++++++++++++ .../src/main/res/values-es/translations.xml | 57 +++++++++++++++++++ .../src/main/res/values-it/translations.xml | 57 +++++++++++++++++++ .../src/main/res/values-ro/translations.xml | 57 +++++++++++++++++++ .../impl/src/main/res/values/localazy.xml | 57 +++++++++++++++++++ .../DefaultRoomLastMessageFormatterTests.kt | 2 +- .../eventformatter/test/build.gradle.kts | 28 +++++++++ .../test}/FakeRoomLastMessageFormatter.kt | 3 +- .../src/main/res/values-de/translations.xml | 5 ++ .../src/main/res/values-de/translations.xml | 3 + .../kotlin/extension/DependencyHandleScope.kt | 1 + samples/minimal/build.gradle.kts | 1 + .../android/samples/minimal/RoomListScreen.kt | 2 +- tools/localazy/config.json | 7 ++- 29 files changed, 406 insertions(+), 259 deletions(-) create mode 100644 libraries/eventformatter/api/build.gradle.kts rename {features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl => libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api}/RoomLastMessageFormatter.kt (93%) create mode 100644 libraries/eventformatter/impl/build.gradle.kts rename {features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist => libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter}/impl/DefaultRoomLastMessageFormatter.kt (99%) create mode 100644 libraries/eventformatter/impl/src/main/res/values-de/translations.xml create mode 100644 libraries/eventformatter/impl/src/main/res/values-es/translations.xml create mode 100644 libraries/eventformatter/impl/src/main/res/values-it/translations.xml create mode 100644 libraries/eventformatter/impl/src/main/res/values-ro/translations.xml create mode 100644 libraries/eventformatter/impl/src/main/res/values/localazy.xml rename {features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist => libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter}/impl/DefaultRoomLastMessageFormatterTests.kt (99%) create mode 100644 libraries/eventformatter/test/build.gradle.kts rename {features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl => libraries/eventformatter/test/src/main/kotlin/io/element/android/libraries/eventformatter/test}/FakeRoomLastMessageFormatter.kt (88%) diff --git a/features/createroom/impl/src/main/res/values-de/translations.xml b/features/createroom/impl/src/main/res/values-de/translations.xml index ec038d764e..210f7f4cb4 100644 --- a/features/createroom/impl/src/main/res/values-de/translations.xml +++ b/features/createroom/impl/src/main/res/values-de/translations.xml @@ -1,8 +1,9 @@ "Neuer Raum" + "Personen einladen" "Personen hinzufügen" "Privater Raum (nur auf Einladung)" "Raumname" "Thema (optional)" - \ No newline at end of file + diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml index 061f3453df..4c9b232147 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,9 @@ + "Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfen Sie, ob Sie die Homeserver-URL korrekt eingegeben haben. Wenn die URL korrekt ist, wenden Sie sich an Ihren Homeserver-Administrator, um weitere Hilfe zu erhalten." + "Dieser Server unterstützt derzeit kein Sliding Sync." + "Homeserver-URL" + "Sie können nur eine Verbindung zu einem vorhandenen Server herstellen, der Sliding Sync unterstützt. Ihr Homeserver-Administrator muss dies konfigurieren. %1$s" "Wie lautet die Adresse deines Servers?" "Willkommen zurück!" "Passwort" diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml index 7581b585f1..4e5b3e9a74 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -4,11 +4,13 @@ "1 Person" "%1$d Personen" + "Bereits eingeladen" "Raum teilen" "Blockieren" "Nutzer blockieren" "Blockierung aufheben" "Nutzer entblockieren" + "Personen einladen" "Raum verlassen" "Sicherheit" "Thema" diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index 100ab15437..c1ee491e7f 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) implementation(projects.libraries.dateformatter.api) + implementation(projects.libraries.eventformatter.api) implementation(projects.features.invitelist.api) implementation(projects.features.networkmonitor.api) implementation(projects.features.leaveroom.api) @@ -63,6 +64,7 @@ dependencies { testImplementation(libs.test.robolectric) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.dateformatter.test) + testImplementation(projects.libraries.eventformatter.test) testImplementation(projects.libraries.permissions.noop) testImplementation(projects.features.invitelist.test) testImplementation(projects.features.networkmonitor.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 e1f37ae5e6..0c54525f00 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 @@ -39,6 +39,7 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.handleSnackbarMessage +import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId diff --git a/features/roomlist/impl/src/main/res/values-de/translations.xml b/features/roomlist/impl/src/main/res/values-de/translations.xml index 00b1431f00..be0109cbe2 100644 --- a/features/roomlist/impl/src/main/res/values-de/translations.xml +++ b/features/roomlist/impl/src/main/res/values-de/translations.xml @@ -1,40 +1,4 @@ "Alle Chats" - "(Avatar wurde ebenfalls geändert)" - "%1$s hat seinen Avatar geändert" - "Du hast deinen Avatar geändert" - "%1$s hat den Anzeigenamen von %2$s in %3$s geändert" - "Du hast deinen Anzeigenamen von %1$s in %2$s geändert" - "%1$s hat den Anzeigenamen entfernt (war %2$s)" - "Du hast deinen Anzeigenamen entfernt (war %1$s)" - "%1$s hat den Anzeigenamen auf %2$s gesetzt" - "Du hast deinen Anzeigenamen auf %1$s gesetzt" - "%1$s hat den Raum-Avatar geändert" - "Du hast den Raum-Avatar geändert" - "%1$s hat den Raum-Avatar entfernt" - "%1$s hat den Raum erstellt" - "Du hast den Raum erstellt" - "%1$s hat %2$s eingeladen" - "%1$s hat die Einladung angenommen" - "Du hast die Einladung angenommen" - "Du hast %1$s eingeladen" - "%1$s hat dich eingeladen" - "%1$s ist dem Raum beigetreten" - "Du bist dem Raum beigetreten" - "%1$s hat deine Beitrittsanfrage abgelehnt" - "%1$s hat den Raum verlassen" - "Du hast den Raum verlassen" - "%1$s hat den Raumnamen geändert in: %2$s" - "Sie haben den Raumnamen geändert in: %1$s" - "%1$s hat den Raumnamen entfernt" - "Du hast den Raumnamen entfernt" - "%1$s hat die Einladung abgelehnt" - "Du hast die Einladung abgelehnt" - "%1$s hat %2$s entfernt" - "Du hast %1$s entfernt" - "%1$s hat das Thema geändert zu: %2$s" - "Sie haben das Thema geändert zu: %1$s" - "%1$s hat das Raumthema entfernt" - "Du hast das Raumthema entfernt" \ No newline at end of file diff --git a/features/roomlist/impl/src/main/res/values-es/translations.xml b/features/roomlist/impl/src/main/res/values-es/translations.xml index 079ffb2e72..7edd6192a1 100644 --- a/features/roomlist/impl/src/main/res/values-es/translations.xml +++ b/features/roomlist/impl/src/main/res/values-es/translations.xml @@ -4,58 +4,4 @@ "Todos los chats" "Parece que estás usando un nuevo dispositivo. Verifica que eres tú para acceder a tus mensajes cifrados." "Accede a tu historial de mensajes" - "(el avatar también cambió)" - "%1$s cambió su avatar" - "Cambiaste tu avatar" - "%1$s cambió su nombre de %2$s a %3$s" - "Cambiaste tu nombre de %1$s a %2$s" - "%1$s eliminó su nombre (era %2$s)" - "Eliminaste tu nombre (era %1$s)" - "%1$s cambió su nombre a %2$s" - "Cambiaste tu nombre a %1$s" - "%1$s cambió el avatar de la sala" - "Cambiaste el avatar de la sala" - "%1$s eliminó el avatar de la sala" - "Eliminaste el avatar de la sala" - "%1$s expulsó permanentemente a %2$s" - "Expulsaste permanentemente a %1$s" - "%1$s creó la sala" - "Tú creaste la sala" - "%1$s invitó a %2$s" - "%1$s aceptó la invitación" - "Aceptaste la invitación" - "Invitaste a %1$s" - "%1$s te invitó." - "%1$s se unió a la sala" - "Te uniste a la sala" - "%1$s solicitó unirse" - "%1$s permitió que %2$s se uniera" - "%1$s te permitió unirte" - "Solicitaste unirte" - "%1$s rechazó la solicitud de %2$s para unirse" - "Rechazaste la solicitud de %1$s para unirte" - "%1$s rechazó su solicitud para unirte" - "%1$s ya no está interesado en unirse" - "Cancelaste tu solicitud de unirte" - "%1$s salió de la sala" - "Saliste de la sala" - "%1$s cambió el nombre de la sala a: %2$s" - "Cambiaste el nombre de la sala a: %1$s" - "%1$s eliminó el nombre de la sala" - "Eliminaste el nombre de la sala" - "%1$s rechazó la invitación" - "Rechazaste la invitación" - "%1$s echó a %2$s" - "Echaste a %1$s" - "%1$s envió una invitación a %2$s para unirse a la sala" - "Enviaste una invitación a %1$s para unirse a la sala" - "%1$s revocó la invitación a %2$s para unirse a la sala" - "Revocaste la invitación de %1$s para unirse a la sala" - "%1$s cambió el tema a: %2$s" - "Cambiaste el tema a: %1$s" - "%1$s eliminó el tema de la sala" - "Eliminaste el tema de la sala" - "%1$s readmitió a %2$s" - "Readmitiste a %1$s" - "%1$s realizó un cambio desconocido en su membresía" \ No newline at end of file diff --git a/features/roomlist/impl/src/main/res/values-it/translations.xml b/features/roomlist/impl/src/main/res/values-it/translations.xml index 20bf487937..6bfb8baa0c 100644 --- a/features/roomlist/impl/src/main/res/values-it/translations.xml +++ b/features/roomlist/impl/src/main/res/values-it/translations.xml @@ -4,58 +4,4 @@ "Tutte le conversazioni" "Sembra che tu stia utilizzando un nuovo dispositivo. Verifica di essere tu per accedere ai tuoi messaggi crittografati." "Accedi alla cronologia dei messaggi" - "(anche l\'avatar è stato cambiato)" - "%1$s ha cambiato il proprio avatar" - "Hai cambiato il tuo avatar" - "%1$s ha cambiato il proprio nome visualizzato da %2$s a %3$s" - "Hai cambiato il tuo nome visualizzato da %1$s a %2$s" - "%1$s ha rimosso il proprio nome visualizzato (era %2$s)" - "Hai rimosso il tuo nome visualizzato (era %1$s)" - "%1$s ha impostato il proprio nome visualizzato su %2$s" - "Hai impostato il tuo nome visualizzato su %1$s" - "%1$s ha cambiato l\'avatar della stanza" - "Hai cambiato l\'avatar della stanza" - "%1$s ha rimosso l\'avatar della stanza" - "Hai rimosso l\'avatar della stanza" - "%1$s ha rimosso %2$s" - "Hai rimosso %1$s" - "%1$s ha creato la stanza" - "Hai creato la stanza" - "%1$s ha invitato %2$s" - "%1$s ha accettato l\'invito" - "Hai accettato l\'invito" - "Hai invitato %1$s" - "%1$s ti ha invitato" - "%1$s si è unito alla stanza" - "Ti sei unito alla stanza" - "%1$s ha chiesto di unirsi" - "%1$s ha permesso a %2$s di unirsi" - "%1$s ti ha permesso di unirti" - "Hai richiesto di unirti" - "%1$s ha rifiutato la richiesta di unirsi di %2$s" - "Hai rifiutato la richiesta di unirsi di %1$s" - "%1$s ha rifiutato la tua richiesta di unirti" - "%1$s non è più interessato a partecipare" - "Hai annullato la tua richiesta di unirti" - "%1$s ha lasciato la stanza" - "Hai lasciato la stanza" - "%1$s ha cambiato il nome della stanza in: %2$s" - "Hai cambiato il nome della stanza in: %1$s" - "%1$s ha rimosso il nome della stanza" - "Hai rimosso il nome della stanza" - "%1$s ha rifiutato l\'invito" - "Hai rifiutato l\'invito" - "%1$s ha rimosso %2$s" - "Hai rimosso %1$s" - "%1$s ha inviato un invito a %2$s per unirsi alla stanza" - "Hai inviato un invito a %1$s per unirsi alla stanza" - "%1$s ha revocato l\'invito di %2$s ad unirsi alla stanza." - "Hai revocato l\'invito a %1$s a universi alla stanza" - "%1$s ha cambiato l\'oggetto in: %2$s" - "Hai cambiato l\'oggetto in: %1$s" - "%1$s ha rimosso l\'oggetto della stanza" - "Hai rimosso l\'oggetto della stanza" - "%1$s ha sbloccato %2$s" - "Hai sbloccato %1$s" - "%1$s ha apportato una modifica sconosciuta alla propria iscrizione" \ No newline at end of file diff --git a/features/roomlist/impl/src/main/res/values-ro/translations.xml b/features/roomlist/impl/src/main/res/values-ro/translations.xml index 89760b3497..7401b30b82 100644 --- a/features/roomlist/impl/src/main/res/values-ro/translations.xml +++ b/features/roomlist/impl/src/main/res/values-ro/translations.xml @@ -4,58 +4,4 @@ "Toate conversatiile" "Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea pentru acces la mesajele dumneavoastră criptate." "Accesați istoricul mesajelor" - "(s-a schimbat si avatarul)" - "%1$s și-a schimbat avatarul" - "V-ați schimbat avatarul" - "%1$s și-a schimbat numele din %2$s în %3$s" - "V-ați schimbat numele din %1$s în %2$s" - "%1$s și-a sters numele (era %2$s)" - "V-ați sters numele (era %1$s)" - "%1$s și-a schimbat numele %2$s" - "V-ați schimbat numele în %1$s" - "%1$s a schimbat avatarul camerei" - "Ați schimbat avatarul camerei" - "%1$s a șters avatarul camerei" - "Ați șters avatarul camerei" - "%1$s a adăugat o interdicție pentru %2$s" - "Ați adăugat o interdicție pentru %1$s" - "%1$s a creat camera" - "Ați creat camera" - "%1$s l-a invitat pe %2$s" - "%1$s a acceptat invitația" - "Ați acceptat invitația" - "L-ați invitat pe %1$s" - "%1$s v-a invitat" - "%1$s a intrat în cameră" - "Ați intrat în cameră" - "%1$s a solicitat să se alăture camerei" - "%1$s i-a permis lui %2$s să se alăture camerei" - "%1$s v-a permis să vă alăturați camerei" - "Ați solicitat să vă alăturați camerei" - "%1$s a respins solicitarea de alăturare a lui %2$s" - "Ați respins solicitarea de alăturare a lui %1$s" - "%1$s a respins cererea dumneavoastră de alăturare" - "%1$s nu mai este interesat să se alăture camerei" - "Ați anulat cererea de alăturare" - "%1$s a părăsit camera" - "Ați părăsit camera" - "%1$s a schimbat numele camerei în: %2$s" - "Ați schimbat numele camerei în: %1$s" - "%1$s a sters numele camerei" - "Ați șters numele camerei" - "%1$s a respins invitația" - "Ați respins invitația" - "%1$s l-a îndepărtat pe %2$s" - "L-ați îndepărtat pe %1$s" - "%1$s a trimis o invitație către %2$s pentru a se alătura camerei" - "Ați trimis o invitație către %1$s pentru a se alătura camerei" - "%1$s a revocat invitația pentru %2$s de a se alătura camerei" - "Ați revocat invitația pentru %1$s de a se alătura camerei" - "%1$s a schimbat subiectul în: %2$s" - "Ați schimbat subiectul în: %1$s" - "%1$s a șters subiectul camerei" - "Ați șters subiectul camerei" - "%1$s a anulat interdicția pentru %2$s" - "Ați anulat interdicția pentru %1$s" - "%1$s a făcut o modificare necunoscută asupra calității sale de membru" \ No newline at end of file diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index 7177e3156d..613e6681ae 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -4,58 +4,4 @@ "All Chats" "Looks like you’re using a new device. Verify it’s you to access your encrypted messages." "Access your message history" - "(avatar was changed too)" - "%1$s changed their avatar" - "You changed your avatar" - "%1$s changed their display name from %2$s to %3$s" - "You changed your display name from %1$s to %2$s" - "%1$s removed their display name (it was %2$s)" - "You removed your display name (it was %1$s)" - "%1$s set their display name to %2$s" - "You set your display name to %1$s" - "%1$s changed the room avatar" - "You changed the room avatar" - "%1$s removed the room avatar" - "You removed the room avatar" - "%1$s banned %2$s" - "You banned %1$s" - "%1$s created the room" - "You created the room" - "%1$s invited %2$s" - "%1$s accepted the invite" - "You accepted the invite" - "You invited %1$s" - "%1$s invited you" - "%1$s joined the room" - "You joined the room" - "%1$s requested to join" - "%1$s allowed %2$s to join" - "%1$s allowed you to join" - "You requested to join" - "%1$s rejected %2$s\'s request to join" - "You rejected %1$s\'s request to join" - "%1$s rejected your request to join" - "%1$s is no longer interested in joining" - "You cancelled your request to join" - "%1$s left the room" - "You left the room" - "%1$s changed the room name to: %2$s" - "You changed the room name to: %1$s" - "%1$s removed the room name" - "You removed the room name" - "%1$s rejected the invitation" - "You rejected the invitation" - "%1$s removed %2$s" - "You removed %1$s" - "%1$s sent an invitation to %2$s to join the room" - "You sent an invitation to %1$s to join the room" - "%1$s revoked the invitation for %2$s to join the room" - "You revoked the invitation for %1$s to join the room" - "%1$s changed the topic to: %2$s" - "You changed the topic to: %1$s" - "%1$s removed the room topic" - "You removed the room topic" - "%1$s unbanned %2$s" - "You unbanned %1$s" - "%1$s made an unknown change to their membership" \ No newline at end of file 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 72996727dd..fe76d9837b 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 @@ -29,6 +29,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.utils.SnackbarDispatcher +import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EXCEPTION diff --git a/libraries/eventformatter/api/build.gradle.kts b/libraries/eventformatter/api/build.gradle.kts new file mode 100644 index 0000000000..ec2a56d780 --- /dev/null +++ b/libraries/eventformatter/api/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.eventformatter.api" +} + +dependencies { + implementation(projects.libraries.matrix.api) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomLastMessageFormatter.kt b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLastMessageFormatter.kt similarity index 93% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomLastMessageFormatter.kt rename to libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLastMessageFormatter.kt index bd59d68592..a7daf55891 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomLastMessageFormatter.kt +++ b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLastMessageFormatter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.roomlist.impl +package io.element.android.libraries.eventformatter.api import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem diff --git a/libraries/eventformatter/impl/build.gradle.kts b/libraries/eventformatter/impl/build.gradle.kts new file mode 100644 index 0000000000..d93043efd1 --- /dev/null +++ b/libraries/eventformatter/impl/build.gradle.kts @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.eventformatter.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + anvil(projects.anvilcodegen) + implementation(projects.anvilannotations) + + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.uiStrings) + api(projects.libraries.eventformatter.api) + + testImplementation(libs.test.junit) + testImplementation(libs.test.robolectric) + testImplementation(libs.test.truth) + testImplementation(projects.libraries.matrix.test) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/DefaultRoomLastMessageFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt similarity index 99% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/DefaultRoomLastMessageFormatter.kt rename to libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt index e2971c93d7..4eeef1c62f 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/DefaultRoomLastMessageFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.roomlist.impl +package io.element.android.libraries.eventformatter.impl import android.content.Context import androidx.compose.ui.text.AnnotatedString @@ -49,6 +49,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecry import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType +import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import timber.log.Timber import javax.inject.Inject import io.element.android.libraries.ui.strings.R as StringR diff --git a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..298bd3d40b --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,39 @@ + + + "(Avatar wurde ebenfalls geändert)" + "%1$s hat seinen Avatar geändert" + "Du hast deinen Avatar geändert" + "%1$s hat den Anzeigenamen von %2$s in %3$s geändert" + "Du hast deinen Anzeigenamen von %1$s in %2$s geändert" + "%1$s hat den Anzeigenamen entfernt (war %2$s)" + "Du hast deinen Anzeigenamen entfernt (war %1$s)" + "%1$s hat den Anzeigenamen auf %2$s gesetzt" + "Du hast deinen Anzeigenamen auf %1$s gesetzt" + "%1$s hat den Raum-Avatar geändert" + "Du hast den Raum-Avatar geändert" + "%1$s hat den Raum-Avatar entfernt" + "%1$s hat den Raum erstellt" + "Du hast den Raum erstellt" + "%1$s hat %2$s eingeladen" + "%1$s hat die Einladung angenommen" + "Du hast die Einladung angenommen" + "Du hast %1$s eingeladen" + "%1$s hat dich eingeladen" + "%1$s ist dem Raum beigetreten" + "Du bist dem Raum beigetreten" + "%1$s hat deine Beitrittsanfrage abgelehnt" + "%1$s hat den Raum verlassen" + "Du hast den Raum verlassen" + "%1$s hat den Raumnamen geändert in: %2$s" + "Sie haben den Raumnamen geändert in: %1$s" + "%1$s hat den Raumnamen entfernt" + "Du hast den Raumnamen entfernt" + "%1$s hat die Einladung abgelehnt" + "Du hast die Einladung abgelehnt" + "%1$s hat %2$s entfernt" + "Du hast %1$s entfernt" + "%1$s hat das Thema geändert zu: %2$s" + "Sie haben das Thema geändert zu: %1$s" + "%1$s hat das Raumthema entfernt" + "Du hast das Raumthema entfernt" + \ No newline at end of file diff --git a/libraries/eventformatter/impl/src/main/res/values-es/translations.xml b/libraries/eventformatter/impl/src/main/res/values-es/translations.xml new file mode 100644 index 0000000000..701f56f41c --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,57 @@ + + + "(el avatar también cambió)" + "%1$s cambió su avatar" + "Cambiaste tu avatar" + "%1$s cambió su nombre de %2$s a %3$s" + "Cambiaste tu nombre de %1$s a %2$s" + "%1$s eliminó su nombre (era %2$s)" + "Eliminaste tu nombre (era %1$s)" + "%1$s cambió su nombre a %2$s" + "Cambiaste tu nombre a %1$s" + "%1$s cambió el avatar de la sala" + "Cambiaste el avatar de la sala" + "%1$s eliminó el avatar de la sala" + "Eliminaste el avatar de la sala" + "%1$s expulsó permanentemente a %2$s" + "Expulsaste permanentemente a %1$s" + "%1$s creó la sala" + "Tú creaste la sala" + "%1$s invitó a %2$s" + "%1$s aceptó la invitación" + "Aceptaste la invitación" + "Invitaste a %1$s" + "%1$s te invitó." + "%1$s se unió a la sala" + "Te uniste a la sala" + "%1$s solicitó unirse" + "%1$s permitió que %2$s se uniera" + "%1$s te permitió unirte" + "Solicitaste unirte" + "%1$s rechazó la solicitud de %2$s para unirse" + "Rechazaste la solicitud de %1$s para unirte" + "%1$s rechazó su solicitud para unirte" + "%1$s ya no está interesado en unirse" + "Cancelaste tu solicitud de unirte" + "%1$s salió de la sala" + "Saliste de la sala" + "%1$s cambió el nombre de la sala a: %2$s" + "Cambiaste el nombre de la sala a: %1$s" + "%1$s eliminó el nombre de la sala" + "Eliminaste el nombre de la sala" + "%1$s rechazó la invitación" + "Rechazaste la invitación" + "%1$s echó a %2$s" + "Echaste a %1$s" + "%1$s envió una invitación a %2$s para unirse a la sala" + "Enviaste una invitación a %1$s para unirse a la sala" + "%1$s revocó la invitación a %2$s para unirse a la sala" + "Revocaste la invitación de %1$s para unirse a la sala" + "%1$s cambió el tema a: %2$s" + "Cambiaste el tema a: %1$s" + "%1$s eliminó el tema de la sala" + "Eliminaste el tema de la sala" + "%1$s readmitió a %2$s" + "Readmitiste a %1$s" + "%1$s realizó un cambio desconocido en su membresía" + \ No newline at end of file diff --git a/libraries/eventformatter/impl/src/main/res/values-it/translations.xml b/libraries/eventformatter/impl/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..0380d802f4 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,57 @@ + + + "(anche l\'avatar è stato cambiato)" + "%1$s ha cambiato il proprio avatar" + "Hai cambiato il tuo avatar" + "%1$s ha cambiato il proprio nome visualizzato da %2$s a %3$s" + "Hai cambiato il tuo nome visualizzato da %1$s a %2$s" + "%1$s ha rimosso il proprio nome visualizzato (era %2$s)" + "Hai rimosso il tuo nome visualizzato (era %1$s)" + "%1$s ha impostato il proprio nome visualizzato su %2$s" + "Hai impostato il tuo nome visualizzato su %1$s" + "%1$s ha cambiato l\'avatar della stanza" + "Hai cambiato l\'avatar della stanza" + "%1$s ha rimosso l\'avatar della stanza" + "Hai rimosso l\'avatar della stanza" + "%1$s ha rimosso %2$s" + "Hai rimosso %1$s" + "%1$s ha creato la stanza" + "Hai creato la stanza" + "%1$s ha invitato %2$s" + "%1$s ha accettato l\'invito" + "Hai accettato l\'invito" + "Hai invitato %1$s" + "%1$s ti ha invitato" + "%1$s si è unito alla stanza" + "Ti sei unito alla stanza" + "%1$s ha chiesto di unirsi" + "%1$s ha permesso a %2$s di unirsi" + "%1$s ti ha permesso di unirti" + "Hai richiesto di unirti" + "%1$s ha rifiutato la richiesta di unirsi di %2$s" + "Hai rifiutato la richiesta di unirsi di %1$s" + "%1$s ha rifiutato la tua richiesta di unirti" + "%1$s non è più interessato a partecipare" + "Hai annullato la tua richiesta di unirti" + "%1$s ha lasciato la stanza" + "Hai lasciato la stanza" + "%1$s ha cambiato il nome della stanza in: %2$s" + "Hai cambiato il nome della stanza in: %1$s" + "%1$s ha rimosso il nome della stanza" + "Hai rimosso il nome della stanza" + "%1$s ha rifiutato l\'invito" + "Hai rifiutato l\'invito" + "%1$s ha rimosso %2$s" + "Hai rimosso %1$s" + "%1$s ha inviato un invito a %2$s per unirsi alla stanza" + "Hai inviato un invito a %1$s per unirsi alla stanza" + "%1$s ha revocato l\'invito di %2$s ad unirsi alla stanza." + "Hai revocato l\'invito a %1$s a universi alla stanza" + "%1$s ha cambiato l\'oggetto in: %2$s" + "Hai cambiato l\'oggetto in: %1$s" + "%1$s ha rimosso l\'oggetto della stanza" + "Hai rimosso l\'oggetto della stanza" + "%1$s ha sbloccato %2$s" + "Hai sbloccato %1$s" + "%1$s ha apportato una modifica sconosciuta alla propria iscrizione" + \ No newline at end of file diff --git a/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..2e3abf93d0 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,57 @@ + + + "(s-a schimbat si avatarul)" + "%1$s și-a schimbat avatarul" + "V-ați schimbat avatarul" + "%1$s și-a schimbat numele din %2$s în %3$s" + "V-ați schimbat numele din %1$s în %2$s" + "%1$s și-a sters numele (era %2$s)" + "V-ați sters numele (era %1$s)" + "%1$s și-a schimbat numele %2$s" + "V-ați schimbat numele în %1$s" + "%1$s a schimbat avatarul camerei" + "Ați schimbat avatarul camerei" + "%1$s a șters avatarul camerei" + "Ați șters avatarul camerei" + "%1$s a adăugat o interdicție pentru %2$s" + "Ați adăugat o interdicție pentru %1$s" + "%1$s a creat camera" + "Ați creat camera" + "%1$s l-a invitat pe %2$s" + "%1$s a acceptat invitația" + "Ați acceptat invitația" + "L-ați invitat pe %1$s" + "%1$s v-a invitat" + "%1$s a intrat în cameră" + "Ați intrat în cameră" + "%1$s a solicitat să se alăture camerei" + "%1$s i-a permis lui %2$s să se alăture camerei" + "%1$s v-a permis să vă alăturați camerei" + "Ați solicitat să vă alăturați camerei" + "%1$s a respins solicitarea de alăturare a lui %2$s" + "Ați respins solicitarea de alăturare a lui %1$s" + "%1$s a respins cererea dumneavoastră de alăturare" + "%1$s nu mai este interesat să se alăture camerei" + "Ați anulat cererea de alăturare" + "%1$s a părăsit camera" + "Ați părăsit camera" + "%1$s a schimbat numele camerei în: %2$s" + "Ați schimbat numele camerei în: %1$s" + "%1$s a sters numele camerei" + "Ați șters numele camerei" + "%1$s a respins invitația" + "Ați respins invitația" + "%1$s l-a îndepărtat pe %2$s" + "L-ați îndepărtat pe %1$s" + "%1$s a trimis o invitație către %2$s pentru a se alătura camerei" + "Ați trimis o invitație către %1$s pentru a se alătura camerei" + "%1$s a revocat invitația pentru %2$s de a se alătura camerei" + "Ați revocat invitația pentru %1$s de a se alătura camerei" + "%1$s a schimbat subiectul în: %2$s" + "Ați schimbat subiectul în: %1$s" + "%1$s a șters subiectul camerei" + "Ați șters subiectul camerei" + "%1$s a anulat interdicția pentru %2$s" + "Ați anulat interdicția pentru %1$s" + "%1$s a făcut o modificare necunoscută asupra calității sale de membru" + \ No newline at end of file diff --git a/libraries/eventformatter/impl/src/main/res/values/localazy.xml b/libraries/eventformatter/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..2fd4217cd4 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values/localazy.xml @@ -0,0 +1,57 @@ + + + "(avatar was changed too)" + "%1$s changed their avatar" + "You changed your avatar" + "%1$s changed their display name from %2$s to %3$s" + "You changed your display name from %1$s to %2$s" + "%1$s removed their display name (it was %2$s)" + "You removed your display name (it was %1$s)" + "%1$s set their display name to %2$s" + "You set your display name to %1$s" + "%1$s changed the room avatar" + "You changed the room avatar" + "%1$s removed the room avatar" + "You removed the room avatar" + "%1$s banned %2$s" + "You banned %1$s" + "%1$s created the room" + "You created the room" + "%1$s invited %2$s" + "%1$s accepted the invite" + "You accepted the invite" + "You invited %1$s" + "%1$s invited you" + "%1$s joined the room" + "You joined the room" + "%1$s requested to join" + "%1$s allowed %2$s to join" + "%1$s allowed you to join" + "You requested to join" + "%1$s rejected %2$s\'s request to join" + "You rejected %1$s\'s request to join" + "%1$s rejected your request to join" + "%1$s is no longer interested in joining" + "You cancelled your request to join" + "%1$s left the room" + "You left the room" + "%1$s changed the room name to: %2$s" + "You changed the room name to: %1$s" + "%1$s removed the room name" + "You removed the room name" + "%1$s rejected the invitation" + "You rejected the invitation" + "%1$s removed %2$s" + "You removed %1$s" + "%1$s sent an invitation to %2$s to join the room" + "You sent an invitation to %1$s to join the room" + "%1$s revoked the invitation for %2$s to join the room" + "You revoked the invitation for %1$s to join the room" + "%1$s changed the topic to: %2$s" + "You changed the topic to: %1$s" + "%1$s removed the room topic" + "You removed the room topic" + "%1$s unbanned %2$s" + "You unbanned %1$s" + "%1$s made an unknown change to their membership" + \ No newline at end of file diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/DefaultRoomLastMessageFormatterTests.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt similarity index 99% rename from features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/DefaultRoomLastMessageFormatterTests.kt rename to libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt index d4c2ca7dd2..8888b2c3eb 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/DefaultRoomLastMessageFormatterTests.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.roomlist.impl +package io.element.android.libraries.eventformatter.impl import android.content.Context import androidx.compose.ui.text.AnnotatedString diff --git a/libraries/eventformatter/test/build.gradle.kts b/libraries/eventformatter/test/build.gradle.kts new file mode 100644 index 0000000000..8250c57247 --- /dev/null +++ b/libraries/eventformatter/test/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.eventformatter.test" +} + +dependencies { + implementation(projects.libraries.eventformatter.api) + implementation(projects.libraries.matrix.api) +} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/FakeRoomLastMessageFormatter.kt b/libraries/eventformatter/test/src/main/kotlin/io/element/android/libraries/eventformatter/test/FakeRoomLastMessageFormatter.kt similarity index 88% rename from features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/FakeRoomLastMessageFormatter.kt rename to libraries/eventformatter/test/src/main/kotlin/io/element/android/libraries/eventformatter/test/FakeRoomLastMessageFormatter.kt index e0763748bf..2293727718 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/FakeRoomLastMessageFormatter.kt +++ b/libraries/eventformatter/test/src/main/kotlin/io/element/android/libraries/eventformatter/test/FakeRoomLastMessageFormatter.kt @@ -14,8 +14,9 @@ * limitations under the License. */ -package io.element.android.features.roomlist.impl +package io.element.android.libraries.eventformatter.test +import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem class FakeRoomLastMessageFormatter : RoomLastMessageFormatter { diff --git a/libraries/push/impl/src/main/res/values-de/translations.xml b/libraries/push/impl/src/main/res/values-de/translations.xml index 8bf4b581dd..d1ce970c3c 100644 --- a/libraries/push/impl/src/main/res/values-de/translations.xml +++ b/libraries/push/impl/src/main/res/values-de/translations.xml @@ -8,11 +8,16 @@ "%1$s: %2$s" "%1$s: %2$s %3$s" "%1$s und %2$s" + "%1$s in %2$s" "%1$s in %2$s und %3$s" "%1$s: %2$d Nachricht" "%1$s: %2$d Nachrichten" + + "%d Mitteilung" + "%d Mitteilungen" + "%d Einladung" "%d Einladungen" 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 4cd9d61ac9..428d42ec14 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -61,8 +61,11 @@ "Offline" "Passwort" "Reaktionen" + "Suchergebnisse" "Sicherheit" + "Server wird nicht unterstützt" "Einstellungen" + "Chat wird gestartet…" "Sticker" "Erfolg" "Vorschläge" diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index c433ed2e07..53fcec10a0 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -74,6 +74,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:matrixui")) implementation(project(":libraries:network")) implementation(project(":libraries:core")) + implementation(project(":libraries:eventformatter:impl")) implementation(project(":libraries:permissions:impl")) implementation(project(":libraries:push:impl")) implementation(project(":libraries:push:impl")) diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 73e09d2314..41390999f0 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.core) implementation(projects.libraries.dateformatter.impl) + implementation(projects.libraries.eventformatter.impl) implementation(projects.features.invitelist.impl) implementation(projects.features.roomlist.impl) implementation(projects.features.leaveroom.impl) 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 9a5e1a8631..b9703aa98a 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 @@ -24,7 +24,6 @@ import io.element.android.features.invitelist.impl.DefaultSeenInvitesStore import io.element.android.features.leaveroom.impl.LeaveRoomPresenterImpl import io.element.android.features.networkmonitor.impl.NetworkMonitorImpl import io.element.android.features.roomlist.impl.DefaultInviteStateDataSource -import io.element.android.features.roomlist.impl.DefaultRoomLastMessageFormatter import io.element.android.features.roomlist.impl.RoomListPresenter import io.element.android.features.roomlist.impl.RoomListView import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -32,6 +31,7 @@ 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.eventformatter.impl.DefaultRoomLastMessageFormatter import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMembershipObserver diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 5219187f75..1e0e125137 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -56,6 +56,12 @@ "error_no_compatible_app_found" ] }, + { + "name": ":libraries:messageformatter:impl", + "includeRegex": [ + "state_event_.*" + ] + }, { "name": ":libraries:push:impl", "includeRegex": [ @@ -73,7 +79,6 @@ { "name": ":features:roomlist:impl", "includeRegex": [ - "state_event_.*", "screen_roomlist_.*", "session_verification_banner_.*" ] From abf7089cea25e8d93c92f1a38c40974551f48b2c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 16 May 2023 12:21:06 +0200 Subject: [PATCH 15/34] Inject StringProvider instead of context. --- .../eventformatter/impl/build.gradle.kts | 2 + .../impl/DefaultRoomLastMessageFormatter.kt | 132 +++++++++--------- .../DefaultRoomLastMessageFormatterTests.kt | 4 +- samples/minimal/build.gradle.kts | 1 + .../android/samples/minimal/RoomListScreen.kt | 3 +- 5 files changed, 73 insertions(+), 69 deletions(-) diff --git a/libraries/eventformatter/impl/build.gradle.kts b/libraries/eventformatter/impl/build.gradle.kts index d93043efd1..e1a1fdb70a 100644 --- a/libraries/eventformatter/impl/build.gradle.kts +++ b/libraries/eventformatter/impl/build.gradle.kts @@ -40,8 +40,10 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.uiStrings) + implementation(projects.services.toolbox.api) api(projects.libraries.eventformatter.api) + testImplementation(projects.services.toolbox.impl) testImplementation(libs.test.junit) testImplementation(libs.test.robolectric) testImplementation(libs.test.truth) 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 4eeef1c62f..3c84166ddf 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 @@ -16,15 +16,14 @@ package io.element.android.libraries.eventformatter.impl -import android.content.Context import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType @@ -49,15 +48,14 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecry import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType -import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter +import io.element.android.services.toolbox.api.strings.StringProvider import timber.log.Timber import javax.inject.Inject import io.element.android.libraries.ui.strings.R as StringR @ContributesBinding(SessionScope::class) class DefaultRoomLastMessageFormatter @Inject constructor( - // TODO replace with StringProvider - @ApplicationContext private val context: Context, + private val sp: StringProvider, private val matrixClient: MatrixClient, ) : RoomLastMessageFormatter { @@ -67,7 +65,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor( return when (val content = event.content) { is MessageContent -> processMessageContents(content, senderDisplayName, isDmRoom) RedactedContent -> { - val message = context.getString(StringR.string.common_message_removed) + val message = sp.getString(StringR.string.common_message_removed) if (!isDmRoom) { prefix(message, senderDisplayName) } else { @@ -78,7 +76,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor( content.body } is UnableToDecryptContent -> { - val message = context.getString(StringR.string.common_decryption_error) + val message = sp.getString(StringR.string.common_decryption_error) if (!isDmRoom) { prefix(message, senderDisplayName) } else { @@ -95,7 +93,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor( processRoomStateChange(content, senderDisplayName, isOutgoing) } is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> { - prefixIfNeeded(context.getString(StringR.string.common_unsupported_event), senderDisplayName, isDmRoom) + prefixIfNeeded(sp.getString(StringR.string.common_unsupported_event), senderDisplayName, isDmRoom) } } } @@ -112,19 +110,19 @@ class DefaultRoomLastMessageFormatter @Inject constructor( messageType.body } is VideoMessageType -> { - context.getString(StringR.string.common_video) + sp.getString(StringR.string.common_video) } is ImageMessageType -> { - context.getString(StringR.string.common_image) + sp.getString(StringR.string.common_image) } is FileMessageType -> { - context.getString(StringR.string.common_file) + sp.getString(StringR.string.common_file) } is AudioMessageType -> { - context.getString(StringR.string.common_audio) + sp.getString(StringR.string.common_audio) } UnknownMessageType -> { - context.getString(StringR.string.common_unsupported_event) + sp.getString(StringR.string.common_unsupported_event) } is NoticeMessageType -> { messageType.body @@ -138,73 +136,73 @@ class DefaultRoomLastMessageFormatter @Inject constructor( val memberIsYou = userId == matrixClient.sessionId return when (val change = membershipContent.change) { MembershipChange.JOINED -> if (memberIsYou) { - context.getString(R.string.state_event_room_join_by_you) + sp.getString(R.string.state_event_room_join_by_you) } else { - context.getString(R.string.state_event_room_join, userId.value) + sp.getString(R.string.state_event_room_join, userId.value) } MembershipChange.LEFT -> if (memberIsYou) { - context.getString(R.string.state_event_room_leave_by_you) + sp.getString(R.string.state_event_room_leave_by_you) } else { - context.getString(R.string.state_event_room_leave, userId.value) + sp.getString(R.string.state_event_room_leave, userId.value) } MembershipChange.BANNED, MembershipChange.KICKED_AND_BANNED -> if (senderIsYou) { - context.getString(R.string.state_event_room_ban_by_you, userId.value) + sp.getString(R.string.state_event_room_ban_by_you, userId.value) } else { - context.getString(R.string.state_event_room_ban, senderDisplayName, userId.value) + sp.getString(R.string.state_event_room_ban, senderDisplayName, userId.value) } MembershipChange.UNBANNED -> if (senderIsYou) { - context.getString(R.string.state_event_room_unban_by_you, userId.value) + sp.getString(R.string.state_event_room_unban_by_you, userId.value) } else { - context.getString(R.string.state_event_room_unban, senderDisplayName, userId.value) + sp.getString(R.string.state_event_room_unban, senderDisplayName, userId.value) } MembershipChange.KICKED -> if (senderIsYou) { - context.getString(R.string.state_event_room_remove_by_you, userId.value) + sp.getString(R.string.state_event_room_remove_by_you, userId.value) } else { - context.getString(R.string.state_event_room_remove, senderDisplayName, userId.value) + sp.getString(R.string.state_event_room_remove, senderDisplayName, userId.value) } MembershipChange.INVITED -> if (senderIsYou) { - context.getString(R.string.state_event_room_invite_by_you, userId.value) + sp.getString(R.string.state_event_room_invite_by_you, userId.value) } else if (memberIsYou) { - context.getString(R.string.state_event_room_invite_you, senderDisplayName) + sp.getString(R.string.state_event_room_invite_you, senderDisplayName) } else { - context.getString(R.string.state_event_room_invite, senderDisplayName, userId.value) + sp.getString(R.string.state_event_room_invite, senderDisplayName, userId.value) } MembershipChange.INVITATION_ACCEPTED -> if (memberIsYou) { - context.getString(R.string.state_event_room_invite_accepted_by_you) + sp.getString(R.string.state_event_room_invite_accepted_by_you) } else { - context.getString(R.string.state_event_room_invite_accepted, userId.value) + sp.getString(R.string.state_event_room_invite_accepted, userId.value) } MembershipChange.INVITATION_REJECTED -> if (memberIsYou) { - context.getString(R.string.state_event_room_reject_by_you) + sp.getString(R.string.state_event_room_reject_by_you) } else { - context.getString(R.string.state_event_room_reject, userId.value) + sp.getString(R.string.state_event_room_reject, userId.value) } MembershipChange.INVITATION_REVOKED -> if (senderIsYou) { - context.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userId.value) + sp.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userId.value) } else { - context.getString(R.string.state_event_room_third_party_revoked_invite, senderDisplayName, userId.value) + sp.getString(R.string.state_event_room_third_party_revoked_invite, senderDisplayName, userId.value) } MembershipChange.KNOCKED -> if (memberIsYou) { - context.getString(R.string.state_event_room_knock_by_you) + sp.getString(R.string.state_event_room_knock_by_you) } else { - context.getString(R.string.state_event_room_knock, userId.value) + sp.getString(R.string.state_event_room_knock, userId.value) } MembershipChange.KNOCK_ACCEPTED -> if (senderIsYou) { - context.getString(R.string.state_event_room_knock_accepted_by_you, userId.value) + sp.getString(R.string.state_event_room_knock_accepted_by_you, userId.value) } else { - context.getString(R.string.state_event_room_knock_accepted, senderDisplayName, userId.value) + sp.getString(R.string.state_event_room_knock_accepted, senderDisplayName, userId.value) } MembershipChange.KNOCK_RETRACTED -> if (memberIsYou) { - context.getString(R.string.state_event_room_knock_retracted_by_you) + sp.getString(R.string.state_event_room_knock_retracted_by_you) } else { - context.getString(R.string.state_event_room_knock_retracted, userId.value) + sp.getString(R.string.state_event_room_knock_retracted, userId.value) } MembershipChange.KNOCK_DENIED -> if (senderIsYou) { - context.getString(R.string.state_event_room_knock_denied_by_you, userId.value) + sp.getString(R.string.state_event_room_knock_denied_by_you, userId.value) } else if (memberIsYou) { - context.getString(R.string.state_event_room_knock_denied_you, senderDisplayName) + sp.getString(R.string.state_event_room_knock_denied_you, senderDisplayName) } else { - context.getString(R.string.state_event_room_knock_denied, senderDisplayName, userId.value) + sp.getString(R.string.state_event_room_knock_denied, senderDisplayName, userId.value) } else -> { Timber.v("Filtering timeline item for room membership: $membershipContent") @@ -218,27 +216,27 @@ class DefaultRoomLastMessageFormatter @Inject constructor( is OtherState.RoomAvatar -> { val hasAvatarUrl = content.url != null when { - senderIsYou && hasAvatarUrl -> context.getString(R.string.state_event_room_avatar_changed_by_you) - senderIsYou && !hasAvatarUrl -> context.getString(R.string.state_event_room_avatar_removed_by_you) - !senderIsYou && hasAvatarUrl -> context.getString(R.string.state_event_room_avatar_changed, senderDisplayName) - else -> context.getString(R.string.state_event_room_avatar_removed, senderDisplayName) + senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed_by_you) + senderIsYou && !hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_removed_by_you) + !senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed, senderDisplayName) + else -> sp.getString(R.string.state_event_room_avatar_removed, senderDisplayName) } } is OtherState.RoomCreate -> { if (senderIsYou) { - context.getString(R.string.state_event_room_created_by_you) + sp.getString(R.string.state_event_room_created_by_you) } else { - context.getString(R.string.state_event_room_created, senderDisplayName) + sp.getString(R.string.state_event_room_created, senderDisplayName) } } - is OtherState.RoomEncryption -> context.getString(StringR.string.common_encryption_enabled) + is OtherState.RoomEncryption -> sp.getString(StringR.string.common_encryption_enabled) is OtherState.RoomName -> { val hasRoomName = content.name != null when { - senderIsYou && hasRoomName -> context.getString(R.string.state_event_room_name_changed_by_you, content.name) - senderIsYou && !hasRoomName -> context.getString(R.string.state_event_room_name_removed_by_you) - !senderIsYou && hasRoomName -> context.getString(R.string.state_event_room_name_changed, senderDisplayName, content.name) - else -> context.getString(R.string.state_event_room_name_removed, senderDisplayName) + senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed_by_you, content.name) + senderIsYou && !hasRoomName -> sp.getString(R.string.state_event_room_name_removed_by_you) + !senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed, senderDisplayName, content.name) + else -> sp.getString(R.string.state_event_room_name_removed, senderDisplayName) } } is OtherState.RoomThirdPartyInvite -> { @@ -247,18 +245,18 @@ class DefaultRoomLastMessageFormatter @Inject constructor( return null } if (senderIsYou) { - context.getString(R.string.state_event_room_third_party_invite_by_you, content.displayName) + sp.getString(R.string.state_event_room_third_party_invite_by_you, content.displayName) } else { - context.getString(R.string.state_event_room_third_party_invite, senderDisplayName, content.displayName) + sp.getString(R.string.state_event_room_third_party_invite, senderDisplayName, content.displayName) } } is OtherState.RoomTopic -> { val hasRoomTopic = content.topic != null when { - senderIsYou && hasRoomTopic -> context.getString(R.string.state_event_room_topic_changed_by_you, content.topic) - senderIsYou && !hasRoomTopic -> context.getString(R.string.state_event_room_topic_removed_by_you) - !senderIsYou && hasRoomTopic -> context.getString(R.string.state_event_room_topic_changed, senderDisplayName, content.topic) - else -> context.getString(R.string.state_event_room_topic_removed, senderDisplayName) + senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed_by_you, content.topic) + senderIsYou && !hasRoomTopic -> sp.getString(R.string.state_event_room_topic_removed_by_you) + !senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed, senderDisplayName, content.topic) + else -> sp.getString(R.string.state_event_room_topic_removed, senderDisplayName) } } else -> { @@ -278,35 +276,35 @@ class DefaultRoomLastMessageFormatter @Inject constructor( return when { avatarChanged && displayNameChanged -> { val message = processProfileChangeContent(profileChangeContent.copy(avatarUrl = null, prevAvatarUrl = null), senderDisplayName, senderIsYou) - val avatarChangedToo = context.getString(R.string.state_event_avatar_changed_too) + val avatarChangedToo = sp.getString(R.string.state_event_avatar_changed_too) "$message\n$avatarChangedToo" } displayNameChanged -> { if (displayName != null && prevDisplayName != null) { if (senderIsYou) { - context.getString(R.string.state_event_display_name_changed_from_by_you, prevDisplayName, displayName) + sp.getString(R.string.state_event_display_name_changed_from_by_you, prevDisplayName, displayName) } else { - context.getString(R.string.state_event_display_name_changed_from, senderDisplayName, prevDisplayName, displayName) + sp.getString(R.string.state_event_display_name_changed_from, senderDisplayName, prevDisplayName, displayName) } } else if (displayName != null) { if (senderIsYou) { - context.getString(R.string.state_event_display_name_set_by_you, displayName) + sp.getString(R.string.state_event_display_name_set_by_you, displayName) } else { - context.getString(R.string.state_event_display_name_set, senderDisplayName, displayName) + sp.getString(R.string.state_event_display_name_set, senderDisplayName, displayName) } } else { if (senderIsYou) { - context.getString(R.string.state_event_display_name_removed_by_you, prevDisplayName) + sp.getString(R.string.state_event_display_name_removed_by_you, prevDisplayName) } else { - context.getString(R.string.state_event_display_name_removed, senderDisplayName, prevDisplayName) + sp.getString(R.string.state_event_display_name_removed, senderDisplayName, prevDisplayName) } } } avatarChanged -> { if (senderIsYou) { - context.getString(R.string.state_event_avatar_url_changed_by_you) + sp.getString(R.string.state_event_avatar_url_changed_by_you) } else { - context.getString(R.string.state_event_avatar_url_changed, senderDisplayName) + sp.getString(R.string.state_event_avatar_url_changed, senderDisplayName) } } else -> null 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 8888b2c3eb..4fb7fc3e9c 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 @@ -48,6 +48,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.aProfileChangeMessageContent import io.element.android.libraries.matrix.test.room.anEventTimelineItem +import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -66,7 +67,7 @@ class DefaultRoomLastMessageFormatterTests { fun setup() { context = RuntimeEnvironment.getApplication() as Context fakeMatrixClient = FakeMatrixClient() - formatter = DefaultRoomLastMessageFormatter(context, fakeMatrixClient) + formatter = DefaultRoomLastMessageFormatter(AndroidStringProvider(context.resources), fakeMatrixClient) } @Test @@ -145,6 +146,7 @@ class DefaultRoomLastMessageFormatterTests { fun createMessageContent(type: MessageType): MessageContent { return MessageContent(body, null, false, type) } + val sharedContentMessagesTypes = arrayOf( TextMessageType(body, null), VideoMessageType(body, "url", null), diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 41390999f0..7d4b222713 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { implementation(projects.features.leaveroom.impl) implementation(projects.features.login.impl) implementation(projects.features.networkmonitor.impl) + implementation(projects.services.toolbox.impl) implementation(libs.coroutines.core) coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") } 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 b9703aa98a..ace7572858 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 @@ -35,6 +35,7 @@ import io.element.android.libraries.eventformatter.impl.DefaultRoomLastMessageFo import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.datetime.Clock @@ -55,7 +56,7 @@ class RoomListScreen( private val presenter = RoomListPresenter( client = matrixClient, lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters), - roomLastMessageFormatter = DefaultRoomLastMessageFormatter(context, matrixClient), + roomLastMessageFormatter = DefaultRoomLastMessageFormatter(AndroidStringProvider(context.resources), matrixClient), sessionVerificationService = sessionVerificationService, networkMonitor = NetworkMonitorImpl(context), snackbarDispatcher = SnackbarDispatcher(), From a5fc4ffe840b9e1d840df0783d715110dfe8a035 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 16 May 2023 12:23:45 +0200 Subject: [PATCH 16/34] Rename `processMessageItem` to simple `format` --- .../roomlist/impl/RoomListPresenter.kt | 2 +- .../api/RoomLastMessageFormatter.kt | 2 +- .../impl/DefaultRoomLastMessageFormatter.kt | 2 +- .../DefaultRoomLastMessageFormatterTests.kt | 154 +++++++++--------- .../test/FakeRoomLastMessageFormatter.kt | 11 +- 5 files changed, 86 insertions(+), 85 deletions(-) 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 0c54525f00..9448ff4b1b 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 @@ -200,7 +200,7 @@ class RoomListPresenter @Inject constructor( hasUnread = roomSummary.details.unreadNotificationCount > 0, timestamp = lastMessageTimestampFormatter.format(roomSummary.details.lastMessageTimestamp), lastMessage = roomSummary.details.lastMessage?.let { message -> - roomLastMessageFormatter.processMessageItem(message.event, roomSummary.details.isDirect) + roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect) }.orEmpty(), avatarData = avatarData, ) diff --git a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLastMessageFormatter.kt b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLastMessageFormatter.kt index a7daf55891..4dd5978bc6 100644 --- a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLastMessageFormatter.kt +++ b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLastMessageFormatter.kt @@ -19,5 +19,5 @@ package io.element.android.libraries.eventformatter.api import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem interface RoomLastMessageFormatter { - fun processMessageItem(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? + fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? } 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 3c84166ddf..1f18698026 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 @@ -59,7 +59,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor( private val matrixClient: MatrixClient, ) : RoomLastMessageFormatter { - override fun processMessageItem(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? { + override fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? { val isOutgoing = event.sender == matrixClient.sessionId val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value return when (val content = event.content) { 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 4fb7fc3e9c..3016ff4884 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 @@ -77,7 +77,7 @@ class DefaultRoomLastMessageFormatterTests { val senderName = "Someone" sequenceOf(false, true).forEach { isDm -> val message = createRoomEvent(false, senderName, RedactedContent) - val result = formatter.processMessageItem(message, isDm) + val result = formatter.format(message, isDm) if (isDm) { Truth.assertThat(result).isEqualTo(expected) } else { @@ -93,7 +93,7 @@ class DefaultRoomLastMessageFormatterTests { val body = "body" val info = ImageInfo(null, null, null, null, null, null, null) val message = createRoomEvent(false, null, StickerContent(body, info, "url")) - val result = formatter.processMessageItem(message, false) + val result = formatter.format(message, false) Truth.assertThat(result).isEqualTo(body) } @@ -104,7 +104,7 @@ class DefaultRoomLastMessageFormatterTests { val senderName = "Someone" sequenceOf(false, true).forEach { isDm -> val message = createRoomEvent(false, senderName, UnableToDecryptContent(UnableToDecryptContent.Data.Unknown)) - val result = formatter.processMessageItem(message, isDm) + val result = formatter.format(message, isDm) if (isDm) { Truth.assertThat(result).isEqualTo(expected) } else { @@ -126,7 +126,7 @@ class DefaultRoomLastMessageFormatterTests { UnknownContent, ).forEach { type -> val message = createRoomEvent(false, senderName, type) - val result = formatter.processMessageItem(message, isDm) + val result = formatter.format(message, isDm) if (isDm) { Truth.assertWithMessage("$type was not properly handled").that(result).isEqualTo(expected) } else { @@ -165,7 +165,7 @@ class DefaultRoomLastMessageFormatterTests { sharedContentMessagesTypes.forEach { type -> val content = createMessageContent(type) val message = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = content) - val result = formatter.processMessageItem(message, isDmRoom = isDm) + val result = formatter.format(message, isDmRoom = isDm) if (isDm) { resultsInDm.add(type to result) } else { @@ -173,7 +173,7 @@ class DefaultRoomLastMessageFormatterTests { } } val unknownMessage = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = createMessageContent(UnknownMessageType)) - val result = UnknownMessageType to formatter.processMessageItem(unknownMessage, isDmRoom = isDm) + val result = UnknownMessageType to formatter.format(unknownMessage, isDmRoom = isDm) if (isDm) { resultsInDm.add(result) } else { @@ -237,11 +237,11 @@ class DefaultRoomLastMessageFormatterTests { val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.JOINED) val youJoinedRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youJoinedRoom = formatter.processMessageItem(youJoinedRoomEvent, false) + val youJoinedRoom = formatter.format(youJoinedRoomEvent, false) Truth.assertThat(youJoinedRoom).isEqualTo("You joined the room") val someoneJoinedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneJoinedRoom = formatter.processMessageItem(someoneJoinedRoomEvent, false) + val someoneJoinedRoom = formatter.format(someoneJoinedRoomEvent, false) Truth.assertThat(someoneJoinedRoom).isEqualTo("${someoneContent.userId} joined the room") } @@ -253,11 +253,11 @@ class DefaultRoomLastMessageFormatterTests { val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.LEFT) val youLeftRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youLeftRoom = formatter.processMessageItem(youLeftRoomEvent, false) + val youLeftRoom = formatter.format(youLeftRoomEvent, false) Truth.assertThat(youLeftRoom).isEqualTo("You left the room") val someoneLeftRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneLeftRoom = formatter.processMessageItem(someoneLeftRoomEvent, false) + val someoneLeftRoom = formatter.format(someoneLeftRoomEvent, false) Truth.assertThat(someoneLeftRoom).isEqualTo("${someoneContent.userId} left the room") } @@ -271,19 +271,19 @@ class DefaultRoomLastMessageFormatterTests { val someoneKickedContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KICKED_AND_BANNED) val youBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youBanned = formatter.processMessageItem(youBannedEvent, false) + val youBanned = formatter.format(youBannedEvent, false) Truth.assertThat(youBanned).isEqualTo("You banned ${youContent.userId}") val youKickBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youKickedContent) - val youKickedBanned = formatter.processMessageItem(youKickBannedEvent, false) + val youKickedBanned = formatter.format(youKickBannedEvent, false) Truth.assertThat(youKickedBanned).isEqualTo("You banned ${youContent.userId}") val someoneBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneBanned = formatter.processMessageItem(someoneBannedEvent, false) + val someoneBanned = formatter.format(someoneBannedEvent, false) Truth.assertThat(someoneBanned).isEqualTo("$otherName banned ${someoneContent.userId}") val someoneKickBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneKickedContent) - val someoneKickBanned = formatter.processMessageItem(someoneKickBannedEvent, false) + val someoneKickBanned = formatter.format(someoneKickBannedEvent, false) Truth.assertThat(someoneKickBanned).isEqualTo("$otherName banned ${someoneContent.userId}") } @@ -295,11 +295,11 @@ class DefaultRoomLastMessageFormatterTests { val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.UNBANNED) val youUnbannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youUnbanned = formatter.processMessageItem(youUnbannedEvent, false) + val youUnbanned = formatter.format(youUnbannedEvent, false) Truth.assertThat(youUnbanned).isEqualTo("You unbanned ${youContent.userId}") val someoneUnbannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneUnbanned = formatter.processMessageItem(someoneUnbannedEvent, false) + val someoneUnbanned = formatter.format(someoneUnbannedEvent, false) Truth.assertThat(someoneUnbanned).isEqualTo("$otherName unbanned ${someoneContent.userId}") } @@ -311,11 +311,11 @@ class DefaultRoomLastMessageFormatterTests { val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KICKED) val youKickedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youKicked = formatter.processMessageItem(youKickedEvent, false) + val youKicked = formatter.format(youKickedEvent, false) Truth.assertThat(youKicked).isEqualTo("You removed ${youContent.userId}") val someoneKickedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneKicked = formatter.processMessageItem(someoneKickedEvent, false) + val someoneKicked = formatter.format(someoneKickedEvent, false) Truth.assertThat(someoneKicked).isEqualTo("$otherName removed ${someoneContent.userId}") } @@ -327,15 +327,15 @@ class DefaultRoomLastMessageFormatterTests { val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.INVITED) val youWereInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent) - val youWereInvited = formatter.processMessageItem(youWereInvitedEvent, false) + val youWereInvited = formatter.format(youWereInvitedEvent, false) Truth.assertThat(youWereInvited).isEqualTo("$otherName invited you") val youInvitedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) - val youInvited = formatter.processMessageItem(youInvitedEvent, false) + val youInvited = formatter.format(youInvitedEvent, false) Truth.assertThat(youInvited).isEqualTo("You invited ${someoneContent.userId}") val someoneInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneInvited = formatter.processMessageItem(someoneInvitedEvent, false) + val someoneInvited = formatter.format(someoneInvitedEvent, false) Truth.assertThat(someoneInvited).isEqualTo("$otherName invited ${someoneContent.userId}") } @@ -347,11 +347,11 @@ class DefaultRoomLastMessageFormatterTests { val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.INVITATION_ACCEPTED) val youAcceptedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youAcceptedInvite = formatter.processMessageItem(youAcceptedInviteEvent, false) + val youAcceptedInvite = formatter.format(youAcceptedInviteEvent, false) Truth.assertThat(youAcceptedInvite).isEqualTo("You accepted the invite") val someoneAcceptedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneAcceptedInvite = formatter.processMessageItem(someoneAcceptedInviteEvent, false) + val someoneAcceptedInvite = formatter.format(someoneAcceptedInviteEvent, false) Truth.assertThat(someoneAcceptedInvite).isEqualTo("${someoneContent.userId} accepted the invite") } @@ -363,11 +363,11 @@ class DefaultRoomLastMessageFormatterTests { val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.INVITATION_REJECTED) val youRejectedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youRejectedInvite = formatter.processMessageItem(youRejectedInviteEvent, false) + val youRejectedInvite = formatter.format(youRejectedInviteEvent, false) Truth.assertThat(youRejectedInvite).isEqualTo("You rejected the invitation") val someoneRejectedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneRejectedInvite = formatter.processMessageItem(someoneRejectedInviteEvent, false) + val someoneRejectedInvite = formatter.format(someoneRejectedInviteEvent, false) Truth.assertThat(someoneRejectedInvite).isEqualTo("${someoneContent.userId} rejected the invitation") } @@ -378,11 +378,11 @@ class DefaultRoomLastMessageFormatterTests { val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.INVITATION_REVOKED) val youRevokedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) - val youRevokedInvite = formatter.processMessageItem(youRevokedInviteEvent, false) + val youRevokedInvite = formatter.format(youRevokedInviteEvent, false) Truth.assertThat(youRevokedInvite).isEqualTo("You revoked the invitation for ${someoneContent.userId} to join the room") val someoneRevokedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneRevokedInvite = formatter.processMessageItem(someoneRevokedInviteEvent, false) + val someoneRevokedInvite = formatter.format(someoneRevokedInviteEvent, false) Truth.assertThat(someoneRevokedInvite).isEqualTo("$otherName revoked the invitation for ${someoneContent.userId} to join the room") } @@ -394,11 +394,11 @@ class DefaultRoomLastMessageFormatterTests { val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KNOCKED) val youKnockedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youKnocked = formatter.processMessageItem(youKnockedEvent, false) + val youKnocked = formatter.format(youKnockedEvent, false) Truth.assertThat(youKnocked).isEqualTo("You requested to join") val someoneKnockedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneKnocked = formatter.processMessageItem(someoneKnockedEvent, false) + val someoneKnocked = formatter.format(someoneKnockedEvent, false) Truth.assertThat(someoneKnocked).isEqualTo("${someoneContent.userId} requested to join") } @@ -409,11 +409,11 @@ class DefaultRoomLastMessageFormatterTests { val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KNOCK_ACCEPTED) val youAcceptedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) - val youAcceptedKnock = formatter.processMessageItem(youAcceptedKnockEvent, false) + val youAcceptedKnock = formatter.format(youAcceptedKnockEvent, false) Truth.assertThat(youAcceptedKnock).isEqualTo("${someoneContent.userId} allowed you to join") val someoneAcceptedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneAcceptedKnock = formatter.processMessageItem(someoneAcceptedKnockEvent, false) + val someoneAcceptedKnock = formatter.format(someoneAcceptedKnockEvent, false) Truth.assertThat(someoneAcceptedKnock).isEqualTo("$otherName allowed ${someoneContent.userId} to join") } @@ -425,11 +425,11 @@ class DefaultRoomLastMessageFormatterTests { val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KNOCK_RETRACTED) val youRetractedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent) - val youRetractedKnock = formatter.processMessageItem(youRetractedKnockEvent, false) + val youRetractedKnock = formatter.format(youRetractedKnockEvent, false) Truth.assertThat(youRetractedKnock).isEqualTo("You cancelled your request to join") val someoneRetractedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneRetractedKnock = formatter.processMessageItem(someoneRetractedKnockEvent, false) + val someoneRetractedKnock = formatter.format(someoneRetractedKnockEvent, false) Truth.assertThat(someoneRetractedKnock).isEqualTo("${someoneContent.userId} is no longer interested in joining") } @@ -441,15 +441,15 @@ class DefaultRoomLastMessageFormatterTests { val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KNOCK_DENIED) val youDeniedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent) - val youDeniedKnock = formatter.processMessageItem(youDeniedKnockEvent, false) + val youDeniedKnock = formatter.format(youDeniedKnockEvent, false) Truth.assertThat(youDeniedKnock).isEqualTo("You rejected ${someoneContent.userId}'s request to join") val someoneDeniedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent) - val someoneDeniedKnock = formatter.processMessageItem(someoneDeniedKnockEvent, false) + val someoneDeniedKnock = formatter.format(someoneDeniedKnockEvent, false) Truth.assertThat(someoneDeniedKnock).isEqualTo("$otherName rejected ${someoneContent.userId}'s request to join") val someoneDeniedYourKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent) - val someoneDeniedYourKnock = formatter.processMessageItem(someoneDeniedYourKnockEvent, false) + val someoneDeniedYourKnock = formatter.format(someoneDeniedYourKnockEvent, false) Truth.assertThat(someoneDeniedYourKnock).isEqualTo("$otherName rejected your request to join") } @@ -461,7 +461,7 @@ class DefaultRoomLastMessageFormatterTests { val results = otherChanges.map { change -> val content = RoomMembershipContent(A_USER_ID, change) val event = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = content) - val result = formatter.processMessageItem(event, false) + val result = formatter.format(event, false) change to result } val expected = otherChanges.map { it to null } @@ -480,19 +480,19 @@ class DefaultRoomLastMessageFormatterTests { val removedContent = StateContent("", OtherState.RoomAvatar(null)) val youChangedRoomAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youChangedRoomAvatar = formatter.processMessageItem(youChangedRoomAvatarEvent, false) + val youChangedRoomAvatar = formatter.format(youChangedRoomAvatarEvent, false) Truth.assertThat(youChangedRoomAvatar).isEqualTo("You changed the room avatar") val someoneChangedRoomAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) - val someoneChangedRoomAvatar = formatter.processMessageItem(someoneChangedRoomAvatarEvent, false) + val someoneChangedRoomAvatar = formatter.format(someoneChangedRoomAvatarEvent, false) Truth.assertThat(someoneChangedRoomAvatar).isEqualTo("$otherName changed the room avatar") val youRemovedRoomAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) - val youRemovedRoomAvatar = formatter.processMessageItem(youRemovedRoomAvatarEvent, false) + val youRemovedRoomAvatar = formatter.format(youRemovedRoomAvatarEvent, false) Truth.assertThat(youRemovedRoomAvatar).isEqualTo("You removed the room avatar") val someoneRemovedRoomAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) - val someoneRemovedRoomAvatar = formatter.processMessageItem(someoneRemovedRoomAvatarEvent, false) + val someoneRemovedRoomAvatar = formatter.format(someoneRemovedRoomAvatarEvent, false) Truth.assertThat(someoneRemovedRoomAvatar).isEqualTo("$otherName removed the room avatar") } @@ -503,11 +503,11 @@ class DefaultRoomLastMessageFormatterTests { val content = StateContent("", OtherState.RoomCreate) val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content) - val youCreatedRoom = formatter.processMessageItem(youCreatedRoomMessage, false) + val youCreatedRoom = formatter.format(youCreatedRoomMessage, false) Truth.assertThat(youCreatedRoom).isEqualTo("You created the room") val someoneCreatedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = content) - val someoneCreatedRoom = formatter.processMessageItem(someoneCreatedRoomEvent, false) + val someoneCreatedRoom = formatter.format(someoneCreatedRoomEvent, false) Truth.assertThat(someoneCreatedRoom).isEqualTo("$otherName created the room") } @@ -518,11 +518,11 @@ class DefaultRoomLastMessageFormatterTests { val content = StateContent("", OtherState.RoomEncryption) val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content) - val youCreatedRoom = formatter.processMessageItem(youCreatedRoomMessage, false) + val youCreatedRoom = formatter.format(youCreatedRoomMessage, false) Truth.assertThat(youCreatedRoom).isEqualTo("Encryption enabled") val someoneCreatedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = content) - val someoneCreatedRoom = formatter.processMessageItem(someoneCreatedRoomEvent, false) + val someoneCreatedRoom = formatter.format(someoneCreatedRoomEvent, false) Truth.assertThat(someoneCreatedRoom).isEqualTo("Encryption enabled") } @@ -535,19 +535,19 @@ class DefaultRoomLastMessageFormatterTests { val removedContent = StateContent("", OtherState.RoomName(null)) val youChangedRoomNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youChangedRoomName = formatter.processMessageItem(youChangedRoomNameEvent, false) + val youChangedRoomName = formatter.format(youChangedRoomNameEvent, false) Truth.assertThat(youChangedRoomName).isEqualTo("You changed the room name to: $newName") val someoneChangedRoomNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) - val someoneChangedRoomName = formatter.processMessageItem(someoneChangedRoomNameEvent, false) + val someoneChangedRoomName = formatter.format(someoneChangedRoomNameEvent, false) Truth.assertThat(someoneChangedRoomName).isEqualTo("$otherName changed the room name to: $newName") val youRemovedRoomNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) - val youRemovedRoomName = formatter.processMessageItem(youRemovedRoomNameEvent, false) + val youRemovedRoomName = formatter.format(youRemovedRoomNameEvent, false) Truth.assertThat(youRemovedRoomName).isEqualTo("You removed the room name") val someoneRemovedRoomNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) - val someoneRemovedRoomName = formatter.processMessageItem(someoneRemovedRoomNameEvent, false) + val someoneRemovedRoomName = formatter.format(someoneRemovedRoomNameEvent, false) Truth.assertThat(someoneRemovedRoomName).isEqualTo("$otherName removed the room name") } @@ -560,19 +560,19 @@ class DefaultRoomLastMessageFormatterTests { val removedContent = StateContent("", OtherState.RoomThirdPartyInvite(null)) val youInvitedSomeoneEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youInvitedSomeone = formatter.processMessageItem(youInvitedSomeoneEvent, false) + val youInvitedSomeone = formatter.format(youInvitedSomeoneEvent, false) Truth.assertThat(youInvitedSomeone).isEqualTo("You sent an invitation to $inviteeName to join the room") val someoneInvitedSomeoneEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) - val someoneInvitedSomeone = formatter.processMessageItem(someoneInvitedSomeoneEvent, false) + val someoneInvitedSomeone = formatter.format(someoneInvitedSomeoneEvent, false) Truth.assertThat(someoneInvitedSomeone).isEqualTo("$otherName sent an invitation to $inviteeName to join the room") val youInvitedNoOneEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) - val youInvitedNoOne = formatter.processMessageItem(youInvitedNoOneEvent, false) + val youInvitedNoOne = formatter.format(youInvitedNoOneEvent, false) Truth.assertThat(youInvitedNoOne).isNull() val someoneInvitedNoOneEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) - val someoneInvitedNoOne = formatter.processMessageItem(someoneInvitedNoOneEvent, false) + val someoneInvitedNoOne = formatter.format(someoneInvitedNoOneEvent, false) Truth.assertThat(someoneInvitedNoOne).isNull() } @@ -585,19 +585,19 @@ class DefaultRoomLastMessageFormatterTests { val removedContent = StateContent("", OtherState.RoomTopic(null)) val youChangedRoomTopicEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youChangedRoomTopic = formatter.processMessageItem(youChangedRoomTopicEvent, false) + val youChangedRoomTopic = formatter.format(youChangedRoomTopicEvent, false) Truth.assertThat(youChangedRoomTopic).isEqualTo("You changed the topic to: $roomTopic") val someoneChangedRoomTopicEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) - val someoneChangedRoomTopic = formatter.processMessageItem(someoneChangedRoomTopicEvent, false) + val someoneChangedRoomTopic = formatter.format(someoneChangedRoomTopicEvent, false) Truth.assertThat(someoneChangedRoomTopic).isEqualTo("$otherName changed the topic to: $roomTopic") val youRemovedRoomTopicEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) - val youRemovedRoomTopic = formatter.processMessageItem(youRemovedRoomTopicEvent, false) + val youRemovedRoomTopic = formatter.format(youRemovedRoomTopicEvent, false) Truth.assertThat(youRemovedRoomTopic).isEqualTo("You removed the room topic") val someoneRemovedRoomTopicEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) - val someoneRemovedRoomTopic = formatter.processMessageItem(someoneRemovedRoomTopicEvent, false) + val someoneRemovedRoomTopic = formatter.format(someoneRemovedRoomTopicEvent, false) Truth.assertThat(someoneRemovedRoomTopic).isEqualTo("$otherName removed the room topic") } @@ -613,7 +613,7 @@ class DefaultRoomLastMessageFormatterTests { val results = otherStates.map { state -> val content = StateContent("", state) val event = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = content) - val result = formatter.processMessageItem(event, false) + val result = formatter.format(event, false) state to result } val expected = otherStates.map { it to null } @@ -635,35 +635,35 @@ class DefaultRoomLastMessageFormatterTests { val sameContent = aProfileChangeMessageContent(avatarUrl = "same_avatar_url", prevAvatarUrl = "same_avatar_url") val youChangedAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youChangedAvatar = formatter.processMessageItem(youChangedAvatarEvent, false) + val youChangedAvatar = formatter.format(youChangedAvatarEvent, false) Truth.assertThat(youChangedAvatar).isEqualTo("You changed your avatar") val someoneChangeAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) - val someoneChangeAvatar = formatter.processMessageItem(someoneChangeAvatarEvent, false) + val someoneChangeAvatar = formatter.format(someoneChangeAvatarEvent, false) Truth.assertThat(someoneChangeAvatar).isEqualTo("$otherName changed their avatar") val youSetAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = setContent) - val youSetAvatar = formatter.processMessageItem(youSetAvatarEvent, false) + val youSetAvatar = formatter.format(youSetAvatarEvent, false) Truth.assertThat(youSetAvatar).isEqualTo("You changed your avatar") val someoneSetAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = setContent) - val someoneSetAvatar = formatter.processMessageItem(someoneSetAvatarEvent, false) + val someoneSetAvatar = formatter.format(someoneSetAvatarEvent, false) Truth.assertThat(someoneSetAvatar).isEqualTo("$otherName changed their avatar") val youRemovedAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) - val youRemovedAvatar = formatter.processMessageItem(youRemovedAvatarEvent, false) + val youRemovedAvatar = formatter.format(youRemovedAvatarEvent, false) Truth.assertThat(youRemovedAvatar).isEqualTo("You changed your avatar") val someoneRemovedAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) - val someoneRemovedAvatar = formatter.processMessageItem(someoneRemovedAvatarEvent, false) + val someoneRemovedAvatar = formatter.format(someoneRemovedAvatarEvent, false) Truth.assertThat(someoneRemovedAvatar).isEqualTo("$otherName changed their avatar") val unchangedEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = sameContent) - val unchangedResult = formatter.processMessageItem(unchangedEvent, false) + val unchangedResult = formatter.format(unchangedEvent, false) Truth.assertThat(unchangedResult).isNull() val invalidEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = invalidContent) - val invalidResult = formatter.processMessageItem(invalidEvent, false) + val invalidResult = formatter.format(invalidEvent, false) Truth.assertThat(invalidResult).isNull() } @@ -680,35 +680,35 @@ class DefaultRoomLastMessageFormatterTests { val invalidContent = aProfileChangeMessageContent(displayName = null, prevDisplayName = null) val youChangedDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youChangedDisplayName = formatter.processMessageItem(youChangedDisplayNameEvent, false) + val youChangedDisplayName = formatter.format(youChangedDisplayNameEvent, false) Truth.assertThat(youChangedDisplayName).isEqualTo("You changed your display name from $oldDisplayName to $newDisplayName") val someoneChangedDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent) - val someoneChangedDisplayName = formatter.processMessageItem(someoneChangedDisplayNameEvent, false) + val someoneChangedDisplayName = formatter.format(someoneChangedDisplayNameEvent, false) Truth.assertThat(someoneChangedDisplayName).isEqualTo("$otherName changed their display name from $oldDisplayName to $newDisplayName") val youSetDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = setContent) - val youSetDisplayName = formatter.processMessageItem(youSetDisplayNameEvent, false) + val youSetDisplayName = formatter.format(youSetDisplayNameEvent, false) Truth.assertThat(youSetDisplayName).isEqualTo("You set your display name to $newDisplayName") val someoneSetDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = setContent) - val someoneSetDisplayName = formatter.processMessageItem(someoneSetDisplayNameEvent, false) + val someoneSetDisplayName = formatter.format(someoneSetDisplayNameEvent, false) Truth.assertThat(someoneSetDisplayName).isEqualTo("$otherName set their display name to $newDisplayName") val youRemovedDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent) - val youRemovedDisplayName = formatter.processMessageItem(youRemovedDisplayNameEvent, false) + val youRemovedDisplayName = formatter.format(youRemovedDisplayNameEvent, false) Truth.assertThat(youRemovedDisplayName).isEqualTo("You removed your display name (it was $oldDisplayName)") val someoneRemovedDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent) - val someoneRemovedDisplayName = formatter.processMessageItem(someoneRemovedDisplayNameEvent, false) + val someoneRemovedDisplayName = formatter.format(someoneRemovedDisplayNameEvent, false) Truth.assertThat(someoneRemovedDisplayName).isEqualTo("$otherName removed their display name (it was $oldDisplayName)") val unchangedEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = sameContent) - val unchangedResult = formatter.processMessageItem(unchangedEvent, false) + val unchangedResult = formatter.format(unchangedEvent, false) Truth.assertThat(unchangedResult).isNull() val invalidEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = invalidContent) - val invalidResult = formatter.processMessageItem(invalidEvent, false) + val invalidResult = formatter.format(invalidEvent, false) Truth.assertThat(invalidResult).isNull() } @@ -737,15 +737,15 @@ class DefaultRoomLastMessageFormatterTests { ) val youChangedBothEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent) - val youChangedBoth = formatter.processMessageItem(youChangedBothEvent, false) + val youChangedBoth = formatter.format(youChangedBothEvent, false) Truth.assertThat(youChangedBoth).isEqualTo("You changed your display name from $oldDisplayName to $newDisplayName\n(avatar was changed too)") val invalidContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = invalidContent) - val invalidMessage = formatter.processMessageItem(invalidContentEvent, false) + val invalidMessage = formatter.format(invalidContentEvent, false) Truth.assertThat(invalidMessage).isNull() val sameContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = sameContent) - val sameMessage = formatter.processMessageItem(sameContentEvent, false) + val sameMessage = formatter.format(sameContentEvent, false) Truth.assertThat(sameMessage).isNull() } diff --git a/libraries/eventformatter/test/src/main/kotlin/io/element/android/libraries/eventformatter/test/FakeRoomLastMessageFormatter.kt b/libraries/eventformatter/test/src/main/kotlin/io/element/android/libraries/eventformatter/test/FakeRoomLastMessageFormatter.kt index 2293727718..cd723a27af 100644 --- a/libraries/eventformatter/test/src/main/kotlin/io/element/android/libraries/eventformatter/test/FakeRoomLastMessageFormatter.kt +++ b/libraries/eventformatter/test/src/main/kotlin/io/element/android/libraries/eventformatter/test/FakeRoomLastMessageFormatter.kt @@ -21,12 +21,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline class FakeRoomLastMessageFormatter : RoomLastMessageFormatter { - private var processMessageItemResult: CharSequence? = null - override fun processMessageItem(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? { - return processMessageItemResult + private var result: CharSequence? = null + + override fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? { + return result } - fun givenRoomSummaryResult(result: CharSequence?) { - processMessageItemResult = result + fun givenFormatResult(result: CharSequence?) { + this.result = result } } From b7278f3e3eabc6ac7dc981a98129a59497008b87 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 16 May 2023 15:14:30 +0200 Subject: [PATCH 17/34] Process state event in timeline - WIP --- features/messages/impl/build.gradle.kts | 1 + .../event/TimelineItemContentFactory.kt | 12 +++--- ...TimelineItemContentProfileChangeFactory.kt | 15 ++++--- ...imelineItemContentRoomMembershipFactory.kt | 15 ++++--- .../event/TimelineItemContentStateFactory.kt | 15 ++++--- .../event/TimelineItemEventFactory.kt | 2 +- .../event/TimelineItemProfileChangeContent.kt | 26 ++++++++++++ .../TimelineItemRoomMembershipContent.kt | 26 ++++++++++++ .../event/TimelineItemStateEventContent.kt | 26 ++++++++++++ .../api/TimelineEventFormatter.kt | 23 ++++++++++ .../impl/DefaultTimelineEventFormatter.kt | 42 +++++++++++++++++++ 11 files changed, 181 insertions(+), 22 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemProfileChangeContent.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRoomMembershipContent.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateEventContent.kt create mode 100644 libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/TimelineEventFormatter.kt create mode 100644 libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index d50c02bc82..de41e5aa7f 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation(projects.libraries.textcomposer) implementation(projects.libraries.uiStrings) implementation(projects.libraries.dateformatter.api) + implementation(projects.libraries.eventformatter.api) implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.mediaupload.api) 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 527de007b1..eb6d0e45c0 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 @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.factories.event import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent @@ -26,7 +27,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RedactedConte import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent -import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent import javax.inject.Inject @@ -43,15 +43,15 @@ class TimelineItemContentFactory @Inject constructor( private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory ) { - fun create(itemContent: EventContent): TimelineItemEventContent { - return when (itemContent) { + fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent { + return when (val itemContent = eventTimelineItem.content) { is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent) is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent) is MessageContent -> messageFactory.create(itemContent) - is ProfileChangeContent -> profileChangeFactory.create(itemContent) + is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem) is RedactedContent -> redactedMessageFactory.create(itemContent) - is RoomMembershipContent -> roomMembershipFactory.create(itemContent) - is StateContent -> stateFactory.create(itemContent) + is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem) + is StateContent -> stateFactory.create(eventTimelineItem) is StickerContent -> stickerFactory.create(itemContent) is UnableToDecryptContent -> utdFactory.create(itemContent) is UnknownContent -> TimelineItemUnknownContent diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentProfileChangeFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentProfileChangeFactory.kt index 689bbcbaa1..e54d88326d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentProfileChangeFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentProfileChangeFactory.kt @@ -17,13 +17,18 @@ package io.element.android.features.messages.impl.timeline.factories.event import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent -import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent +import io.element.android.libraries.core.extensions.orEmpty +import io.element.android.libraries.eventformatter.api.TimelineEventFormatter +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import javax.inject.Inject -class TimelineItemContentProfileChangeFactory @Inject constructor() { +class TimelineItemContentProfileChangeFactory @Inject constructor( + private val timelineEventFormatter: TimelineEventFormatter, +) { - fun create(content: ProfileChangeContent): TimelineItemEventContent { - return TimelineItemUnknownContent + fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent { + val text = timelineEventFormatter.format(eventTimelineItem) + return TimelineItemProfileChangeContent(text.orEmpty().toString()) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentRoomMembershipFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentRoomMembershipFactory.kt index 808e0d3b70..d5cf0cec2d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentRoomMembershipFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentRoomMembershipFactory.kt @@ -17,13 +17,18 @@ package io.element.android.features.messages.impl.timeline.factories.event import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent -import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent +import io.element.android.libraries.core.extensions.orEmpty +import io.element.android.libraries.eventformatter.api.TimelineEventFormatter +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import javax.inject.Inject -class TimelineItemContentRoomMembershipFactory @Inject constructor() { +class TimelineItemContentRoomMembershipFactory @Inject constructor( + private val timelineEventFormatter: TimelineEventFormatter, +) { - fun create(content: RoomMembershipContent): TimelineItemEventContent { - return TimelineItemUnknownContent + fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent { + val text = timelineEventFormatter.format(eventTimelineItem) + return TimelineItemRoomMembershipContent(text.orEmpty().toString()) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStateFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStateFactory.kt index 1680006bc1..072b568af9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStateFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStateFactory.kt @@ -17,13 +17,18 @@ package io.element.android.features.messages.impl.timeline.factories.event import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent -import io.element.android.libraries.matrix.api.timeline.item.event.StateContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent +import io.element.android.libraries.core.extensions.orEmpty +import io.element.android.libraries.eventformatter.api.TimelineEventFormatter +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import javax.inject.Inject -class TimelineItemContentStateFactory @Inject constructor() { +class TimelineItemContentStateFactory @Inject constructor( + private val timelineEventFormatter: TimelineEventFormatter, +) { - fun create(content: StateContent): TimelineItemEventContent { - return TimelineItemUnknownContent + fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent { + val text = timelineEventFormatter.format(eventTimelineItem) + return TimelineItemStateEventContent(text.orEmpty().toString()) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index c21622b50f..a69fb8ddcb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -67,7 +67,7 @@ class TimelineItemEventFactory @Inject constructor( senderId = currentSender, senderDisplayName = senderDisplayName, senderAvatar = senderAvatarData, - content = contentFactory.create(currentTimelineItem.event.content), + content = contentFactory.create(currentTimelineItem.event), isMine = currentTimelineItem.event.isOwn, groupPosition = groupPosition, reactionsState = currentTimelineItem.computeReactionsState() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemProfileChangeContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemProfileChangeContent.kt new file mode 100644 index 0000000000..29836dc28c --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemProfileChangeContent.kt @@ -0,0 +1,26 @@ +/* + * 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. + */ + +package io.element.android.features.messages.impl.timeline.model.event + +import org.jsoup.nodes.Document + +data class TimelineItemProfileChangeContent( + override val body: String, + override val htmlDocument: Document? = null +) : TimelineItemTextBasedContent { + override val type: String = "TimelineItemProfileChangeContent" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRoomMembershipContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRoomMembershipContent.kt new file mode 100644 index 0000000000..c18393b7a0 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRoomMembershipContent.kt @@ -0,0 +1,26 @@ +/* + * 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. + */ + +package io.element.android.features.messages.impl.timeline.model.event + +import org.jsoup.nodes.Document + +data class TimelineItemRoomMembershipContent( + override val body: String, + override val htmlDocument: Document? = null +) : TimelineItemTextBasedContent { + override val type: String = "TimelineItemRoomMembershipContent" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateEventContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateEventContent.kt new file mode 100644 index 0000000000..8ac3e5a40b --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateEventContent.kt @@ -0,0 +1,26 @@ +/* + * 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. + */ + +package io.element.android.features.messages.impl.timeline.model.event + +import org.jsoup.nodes.Document + +data class TimelineItemStateEventContent( + override val body: String, + override val htmlDocument: Document? = null +) : TimelineItemTextBasedContent { + override val type: String = "TimelineItemStateEventContent" +} diff --git a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/TimelineEventFormatter.kt b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/TimelineEventFormatter.kt new file mode 100644 index 0000000000..6a966f1aba --- /dev/null +++ b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/TimelineEventFormatter.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.eventformatter.api + +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem + +interface TimelineEventFormatter { + fun format(event: EventTimelineItem): CharSequence? +} diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt new file mode 100644 index 0000000000..74219997ce --- /dev/null +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.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.eventformatter.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter +import io.element.android.libraries.eventformatter.api.TimelineEventFormatter +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +import javax.inject.Inject + +/** + * For now use the same formatter than for the room list using [RoomLastMessageFormatter]. + * We will change this if we want to have a different rendering in the timeline. + */ +@ContributesBinding(SessionScope::class) +class DefaultTimelineEventFormatter @Inject constructor( + private val roomLastMessageFormatter: RoomLastMessageFormatter, +) : TimelineEventFormatter { + + override fun format(event: EventTimelineItem): CharSequence? { + return roomLastMessageFormatter.format( + event, + /* We do not want to distinguish DM and room here */ + isDmRoom = false, + ) + } +} From 66baf1632e521f1a100b902179b2cc734fd81743 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 16 May 2023 17:18:19 +0200 Subject: [PATCH 18/34] Extract to sub classes --- .../eventformatter/impl/build.gradle.kts | 1 + .../impl/DefaultRoomLastMessageFormatter.kt | 193 +-------------- .../impl/DefaultTimelineEventFormatter.kt | 58 ++++- .../impl/ProfileChangeContentFormatter.kt | 70 ++++++ .../impl/RoomMembershipContentFormatter.kt | 113 +++++++++ .../impl/StateContentFormatter.kt | 219 ++++++++++++++++++ .../eventformatter/impl/mode/RenderingMode.kt | 22 ++ .../android/samples/minimal/RoomListScreen.kt | 12 +- 8 files changed, 490 insertions(+), 198 deletions(-) create mode 100644 libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/ProfileChangeContentFormatter.kt create mode 100644 libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RoomMembershipContentFormatter.kt create mode 100644 libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt create mode 100644 libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/mode/RenderingMode.kt diff --git a/libraries/eventformatter/impl/build.gradle.kts b/libraries/eventformatter/impl/build.gradle.kts index e1a1fdb70a..3bee2df488 100644 --- a/libraries/eventformatter/impl/build.gradle.kts +++ b/libraries/eventformatter/impl/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { anvil(projects.anvilcodegen) implementation(projects.anvilannotations) + implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.uiStrings) 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 1f18698026..327c18cb34 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 @@ -24,6 +24,7 @@ import androidx.compose.ui.text.withStyle import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.SessionScope import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter +import io.element.android.libraries.eventformatter.impl.mode.RenderingMode import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType @@ -32,11 +33,9 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParse import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.OtherState 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 @@ -49,7 +48,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnknownConten import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.services.toolbox.api.strings.StringProvider -import timber.log.Timber import javax.inject.Inject import io.element.android.libraries.ui.strings.R as StringR @@ -57,6 +55,9 @@ import io.element.android.libraries.ui.strings.R as StringR class DefaultRoomLastMessageFormatter @Inject constructor( private val sp: StringProvider, private val matrixClient: MatrixClient, + private val roomMembershipContentFormatter: RoomMembershipContentFormatter, + private val profileChangeContentFormatter: ProfileChangeContentFormatter, + private val stateContentFormatter: StateContentFormatter, ) : RoomLastMessageFormatter { override fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? { @@ -84,13 +85,13 @@ class DefaultRoomLastMessageFormatter @Inject constructor( } } is RoomMembershipContent -> { - processRoomMembershipChange(content, senderDisplayName, isOutgoing) + roomMembershipContentFormatter.format(content, senderDisplayName, isOutgoing) } is ProfileChangeContent -> { - processProfileChangeContent(content, senderDisplayName, isOutgoing) + profileChangeContentFormatter.format(content, senderDisplayName, isOutgoing) } is StateContent -> { - processRoomStateChange(content, senderDisplayName, isOutgoing) + stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.RoomList) } is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> { prefixIfNeeded(sp.getString(StringR.string.common_unsupported_event), senderDisplayName, isDmRoom) @@ -131,186 +132,6 @@ class DefaultRoomLastMessageFormatter @Inject constructor( return prefixIfNeeded(internalMessage, senderDisplayName, isDmRoom) } - private fun processRoomMembershipChange(membershipContent: RoomMembershipContent, senderDisplayName: String, senderIsYou: Boolean): CharSequence? { - val userId = membershipContent.userId - val memberIsYou = userId == matrixClient.sessionId - return when (val change = membershipContent.change) { - MembershipChange.JOINED -> if (memberIsYou) { - sp.getString(R.string.state_event_room_join_by_you) - } else { - sp.getString(R.string.state_event_room_join, userId.value) - } - MembershipChange.LEFT -> if (memberIsYou) { - sp.getString(R.string.state_event_room_leave_by_you) - } else { - sp.getString(R.string.state_event_room_leave, userId.value) - } - MembershipChange.BANNED, MembershipChange.KICKED_AND_BANNED -> if (senderIsYou) { - sp.getString(R.string.state_event_room_ban_by_you, userId.value) - } else { - sp.getString(R.string.state_event_room_ban, senderDisplayName, userId.value) - } - MembershipChange.UNBANNED -> if (senderIsYou) { - sp.getString(R.string.state_event_room_unban_by_you, userId.value) - } else { - sp.getString(R.string.state_event_room_unban, senderDisplayName, userId.value) - } - MembershipChange.KICKED -> if (senderIsYou) { - sp.getString(R.string.state_event_room_remove_by_you, userId.value) - } else { - sp.getString(R.string.state_event_room_remove, senderDisplayName, userId.value) - } - MembershipChange.INVITED -> if (senderIsYou) { - sp.getString(R.string.state_event_room_invite_by_you, userId.value) - } else if (memberIsYou) { - sp.getString(R.string.state_event_room_invite_you, senderDisplayName) - } else { - sp.getString(R.string.state_event_room_invite, senderDisplayName, userId.value) - } - MembershipChange.INVITATION_ACCEPTED -> if (memberIsYou) { - sp.getString(R.string.state_event_room_invite_accepted_by_you) - } else { - sp.getString(R.string.state_event_room_invite_accepted, userId.value) - } - MembershipChange.INVITATION_REJECTED -> if (memberIsYou) { - sp.getString(R.string.state_event_room_reject_by_you) - } else { - sp.getString(R.string.state_event_room_reject, userId.value) - } - MembershipChange.INVITATION_REVOKED -> if (senderIsYou) { - sp.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userId.value) - } else { - sp.getString(R.string.state_event_room_third_party_revoked_invite, senderDisplayName, userId.value) - } - MembershipChange.KNOCKED -> if (memberIsYou) { - sp.getString(R.string.state_event_room_knock_by_you) - } else { - sp.getString(R.string.state_event_room_knock, userId.value) - } - MembershipChange.KNOCK_ACCEPTED -> if (senderIsYou) { - sp.getString(R.string.state_event_room_knock_accepted_by_you, userId.value) - } else { - sp.getString(R.string.state_event_room_knock_accepted, senderDisplayName, userId.value) - } - MembershipChange.KNOCK_RETRACTED -> if (memberIsYou) { - sp.getString(R.string.state_event_room_knock_retracted_by_you) - } else { - sp.getString(R.string.state_event_room_knock_retracted, userId.value) - } - MembershipChange.KNOCK_DENIED -> if (senderIsYou) { - sp.getString(R.string.state_event_room_knock_denied_by_you, userId.value) - } else if (memberIsYou) { - sp.getString(R.string.state_event_room_knock_denied_you, senderDisplayName) - } else { - sp.getString(R.string.state_event_room_knock_denied, senderDisplayName, userId.value) - } - else -> { - Timber.v("Filtering timeline item for room membership: $membershipContent") - null - } - } - } - - private fun processRoomStateChange(stateContent: StateContent, senderDisplayName: String, senderIsYou: Boolean): CharSequence? { - return when (val content = stateContent.content) { - is OtherState.RoomAvatar -> { - val hasAvatarUrl = content.url != null - when { - senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed_by_you) - senderIsYou && !hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_removed_by_you) - !senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed, senderDisplayName) - else -> sp.getString(R.string.state_event_room_avatar_removed, senderDisplayName) - } - } - is OtherState.RoomCreate -> { - if (senderIsYou) { - sp.getString(R.string.state_event_room_created_by_you) - } else { - sp.getString(R.string.state_event_room_created, senderDisplayName) - } - } - is OtherState.RoomEncryption -> sp.getString(StringR.string.common_encryption_enabled) - is OtherState.RoomName -> { - val hasRoomName = content.name != null - when { - senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed_by_you, content.name) - senderIsYou && !hasRoomName -> sp.getString(R.string.state_event_room_name_removed_by_you) - !senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed, senderDisplayName, content.name) - else -> sp.getString(R.string.state_event_room_name_removed, senderDisplayName) - } - } - is OtherState.RoomThirdPartyInvite -> { - if (content.displayName == null) { - Timber.e("RoomThirdPartyInvite undisplayable due to missing name") - return null - } - if (senderIsYou) { - sp.getString(R.string.state_event_room_third_party_invite_by_you, content.displayName) - } else { - sp.getString(R.string.state_event_room_third_party_invite, senderDisplayName, content.displayName) - } - } - is OtherState.RoomTopic -> { - val hasRoomTopic = content.topic != null - when { - senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed_by_you, content.topic) - senderIsYou && !hasRoomTopic -> sp.getString(R.string.state_event_room_topic_removed_by_you) - !senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed, senderDisplayName, content.topic) - else -> sp.getString(R.string.state_event_room_topic_removed, senderDisplayName) - } - } - else -> { - Timber.v("Filtering timeline item for room state change: $content") - null - } - } - } - - private fun processProfileChangeContent( - profileChangeContent: ProfileChangeContent, - senderDisplayName: String, - senderIsYou: Boolean - ): String? = profileChangeContent.run { - val displayNameChanged = displayName != prevDisplayName - val avatarChanged = avatarUrl != prevAvatarUrl - return when { - avatarChanged && displayNameChanged -> { - val message = processProfileChangeContent(profileChangeContent.copy(avatarUrl = null, prevAvatarUrl = null), senderDisplayName, senderIsYou) - val avatarChangedToo = sp.getString(R.string.state_event_avatar_changed_too) - "$message\n$avatarChangedToo" - } - displayNameChanged -> { - if (displayName != null && prevDisplayName != null) { - if (senderIsYou) { - sp.getString(R.string.state_event_display_name_changed_from_by_you, prevDisplayName, displayName) - } else { - sp.getString(R.string.state_event_display_name_changed_from, senderDisplayName, prevDisplayName, displayName) - } - } else if (displayName != null) { - if (senderIsYou) { - sp.getString(R.string.state_event_display_name_set_by_you, displayName) - } else { - sp.getString(R.string.state_event_display_name_set, senderDisplayName, displayName) - } - } else { - if (senderIsYou) { - sp.getString(R.string.state_event_display_name_removed_by_you, prevDisplayName) - } else { - sp.getString(R.string.state_event_display_name_removed, senderDisplayName, prevDisplayName) - } - } - } - avatarChanged -> { - if (senderIsYou) { - sp.getString(R.string.state_event_avatar_url_changed_by_you) - } else { - sp.getString(R.string.state_event_avatar_url_changed, senderDisplayName) - } - } - else -> null - } - } - private fun prefixIfNeeded(message: String, senderDisplayName: String, isDmRoom: Boolean): CharSequence = if (isDmRoom) { message } else { diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt index 74219997ce..4bcae94c1e 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt @@ -17,26 +17,62 @@ package io.element.android.libraries.eventformatter.impl import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.api.TimelineEventFormatter +import io.element.android.libraries.eventformatter.impl.mode.RenderingMode +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent +import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent +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 +import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent +import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent +import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent +import io.element.android.libraries.ui.strings.R +import io.element.android.services.toolbox.api.strings.StringProvider import javax.inject.Inject -/** - * For now use the same formatter than for the room list using [RoomLastMessageFormatter]. - * We will change this if we want to have a different rendering in the timeline. - */ @ContributesBinding(SessionScope::class) class DefaultTimelineEventFormatter @Inject constructor( - private val roomLastMessageFormatter: RoomLastMessageFormatter, + private val sp: StringProvider, + private val matrixClient: MatrixClient, + private val buildMeta: BuildMeta, + private val roomMembershipContentFormatter: RoomMembershipContentFormatter, + private val profileChangeContentFormatter: ProfileChangeContentFormatter, + private val stateContentFormatter: StateContentFormatter, ) : TimelineEventFormatter { override fun format(event: EventTimelineItem): CharSequence? { - return roomLastMessageFormatter.format( - event, - /* We do not want to distinguish DM and room here */ - isDmRoom = false, - ) + val isOutgoing = event.sender == matrixClient.sessionId + val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value + return when (val content = event.content) { + is RoomMembershipContent -> { + roomMembershipContentFormatter.format(content, senderDisplayName, isOutgoing) + } + is ProfileChangeContent -> { + profileChangeContentFormatter.format(content, senderDisplayName, isOutgoing) + } + is StateContent -> { + stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.Timeline) + } + RedactedContent, + is StickerContent, + is UnableToDecryptContent, + is MessageContent, + is FailedToParseMessageLikeContent, + is FailedToParseStateContent, + is UnknownContent -> { + if (buildMeta.isDebuggable) { + error("You should not use this formatter for this event: $event") + } + sp.getString(R.string.common_unsupported_event) + } + } } } diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/ProfileChangeContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/ProfileChangeContentFormatter.kt new file mode 100644 index 0000000000..aea43298f9 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/ProfileChangeContentFormatter.kt @@ -0,0 +1,70 @@ +/* + * 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.eventformatter.impl + +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent +import io.element.android.services.toolbox.api.strings.StringProvider +import javax.inject.Inject + +class ProfileChangeContentFormatter @Inject constructor( + private val sp: StringProvider, +) { + fun format( + profileChangeContent: ProfileChangeContent, + senderDisplayName: String, + senderIsYou: Boolean, + ): String? = profileChangeContent.run { + val displayNameChanged = displayName != prevDisplayName + val avatarChanged = avatarUrl != prevAvatarUrl + return when { + avatarChanged && displayNameChanged -> { + val message = format(profileChangeContent.copy(avatarUrl = null, prevAvatarUrl = null), senderDisplayName, senderIsYou) + val avatarChangedToo = sp.getString(R.string.state_event_avatar_changed_too) + "$message\n$avatarChangedToo" + } + displayNameChanged -> { + if (displayName != null && prevDisplayName != null) { + if (senderIsYou) { + sp.getString(R.string.state_event_display_name_changed_from_by_you, prevDisplayName, displayName) + } else { + sp.getString(R.string.state_event_display_name_changed_from, senderDisplayName, prevDisplayName, displayName) + } + } else if (displayName != null) { + if (senderIsYou) { + sp.getString(R.string.state_event_display_name_set_by_you, displayName) + } else { + sp.getString(R.string.state_event_display_name_set, senderDisplayName, displayName) + } + } else { + if (senderIsYou) { + sp.getString(R.string.state_event_display_name_removed_by_you, prevDisplayName) + } else { + sp.getString(R.string.state_event_display_name_removed, senderDisplayName, prevDisplayName) + } + } + } + avatarChanged -> { + if (senderIsYou) { + sp.getString(R.string.state_event_avatar_url_changed_by_you) + } else { + sp.getString(R.string.state_event_avatar_url_changed, senderDisplayName) + } + } + else -> null + } + } +} diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RoomMembershipContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RoomMembershipContentFormatter.kt new file mode 100644 index 0000000000..b5402454f3 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RoomMembershipContentFormatter.kt @@ -0,0 +1,113 @@ +/* + * 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.eventformatter.impl + +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange +import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent +import io.element.android.services.toolbox.api.strings.StringProvider +import timber.log.Timber +import javax.inject.Inject + +class RoomMembershipContentFormatter @Inject constructor( + private val matrixClient: MatrixClient, + private val sp: StringProvider, +) { + fun format( + membershipContent: RoomMembershipContent, + senderDisplayName: String, + senderIsYou: Boolean, + ): CharSequence? { + val userId = membershipContent.userId + val memberIsYou = userId == matrixClient.sessionId + return when (val change = membershipContent.change) { + MembershipChange.JOINED -> if (memberIsYou) { + sp.getString(R.string.state_event_room_join_by_you) + } else { + sp.getString(R.string.state_event_room_join, userId.value) + } + MembershipChange.LEFT -> if (memberIsYou) { + sp.getString(R.string.state_event_room_leave_by_you) + } else { + sp.getString(R.string.state_event_room_leave, userId.value) + } + MembershipChange.BANNED, MembershipChange.KICKED_AND_BANNED -> if (senderIsYou) { + sp.getString(R.string.state_event_room_ban_by_you, userId.value) + } else { + sp.getString(R.string.state_event_room_ban, senderDisplayName, userId.value) + } + MembershipChange.UNBANNED -> if (senderIsYou) { + sp.getString(R.string.state_event_room_unban_by_you, userId.value) + } else { + sp.getString(R.string.state_event_room_unban, senderDisplayName, userId.value) + } + MembershipChange.KICKED -> if (senderIsYou) { + sp.getString(R.string.state_event_room_remove_by_you, userId.value) + } else { + sp.getString(R.string.state_event_room_remove, senderDisplayName, userId.value) + } + MembershipChange.INVITED -> if (senderIsYou) { + sp.getString(R.string.state_event_room_invite_by_you, userId.value) + } else if (memberIsYou) { + sp.getString(R.string.state_event_room_invite_you, senderDisplayName) + } else { + sp.getString(R.string.state_event_room_invite, senderDisplayName, userId.value) + } + MembershipChange.INVITATION_ACCEPTED -> if (memberIsYou) { + sp.getString(R.string.state_event_room_invite_accepted_by_you) + } else { + sp.getString(R.string.state_event_room_invite_accepted, userId.value) + } + MembershipChange.INVITATION_REJECTED -> if (memberIsYou) { + sp.getString(R.string.state_event_room_reject_by_you) + } else { + sp.getString(R.string.state_event_room_reject, userId.value) + } + MembershipChange.INVITATION_REVOKED -> if (senderIsYou) { + sp.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userId.value) + } else { + sp.getString(R.string.state_event_room_third_party_revoked_invite, senderDisplayName, userId.value) + } + MembershipChange.KNOCKED -> if (memberIsYou) { + sp.getString(R.string.state_event_room_knock_by_you) + } else { + sp.getString(R.string.state_event_room_knock, userId.value) + } + MembershipChange.KNOCK_ACCEPTED -> if (senderIsYou) { + sp.getString(R.string.state_event_room_knock_accepted_by_you, userId.value) + } else { + sp.getString(R.string.state_event_room_knock_accepted, senderDisplayName, userId.value) + } + MembershipChange.KNOCK_RETRACTED -> if (memberIsYou) { + sp.getString(R.string.state_event_room_knock_retracted_by_you) + } else { + sp.getString(R.string.state_event_room_knock_retracted, userId.value) + } + MembershipChange.KNOCK_DENIED -> if (senderIsYou) { + sp.getString(R.string.state_event_room_knock_denied_by_you, userId.value) + } else if (memberIsYou) { + sp.getString(R.string.state_event_room_knock_denied_you, senderDisplayName) + } else { + sp.getString(R.string.state_event_room_knock_denied, senderDisplayName, userId.value) + } + else -> { + Timber.v("Filtering timeline item for room membership: $membershipContent") + null + } + } + } +} diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt new file mode 100644 index 0000000000..169ec55a9d --- /dev/null +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt @@ -0,0 +1,219 @@ +/* + * 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.eventformatter.impl + +import io.element.android.libraries.eventformatter.impl.mode.RenderingMode +import io.element.android.libraries.matrix.api.timeline.item.event.OtherState +import io.element.android.libraries.matrix.api.timeline.item.event.StateContent +import io.element.android.services.toolbox.api.strings.StringProvider +import timber.log.Timber +import javax.inject.Inject + +class StateContentFormatter @Inject constructor( + private val sp: StringProvider, +) { + fun format( + stateContent: StateContent, + senderDisplayName: String, + senderIsYou: Boolean, + renderingMode: RenderingMode, + ): CharSequence? { + return when (val content = stateContent.content) { + is OtherState.RoomAvatar -> { + val hasAvatarUrl = content.url != null + when { + senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed_by_you) + senderIsYou && !hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_removed_by_you) + !senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed, senderDisplayName) + else -> sp.getString(R.string.state_event_room_avatar_removed, senderDisplayName) + } + } + is OtherState.RoomCreate -> { + if (senderIsYou) { + sp.getString(R.string.state_event_room_created_by_you) + } else { + sp.getString(R.string.state_event_room_created, senderDisplayName) + } + } + is OtherState.RoomEncryption -> sp.getString(io.element.android.libraries.ui.strings.R.string.common_encryption_enabled) + is OtherState.RoomName -> { + val hasRoomName = content.name != null + when { + senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed_by_you, content.name) + senderIsYou && !hasRoomName -> sp.getString(R.string.state_event_room_name_removed_by_you) + !senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed, senderDisplayName, content.name) + else -> sp.getString(R.string.state_event_room_name_removed, senderDisplayName) + } + } + is OtherState.RoomThirdPartyInvite -> { + if (content.displayName == null) { + Timber.e("RoomThirdPartyInvite undisplayable due to missing name") + return null + } + if (senderIsYou) { + sp.getString(R.string.state_event_room_third_party_invite_by_you, content.displayName) + } else { + sp.getString(R.string.state_event_room_third_party_invite, senderDisplayName, content.displayName) + } + } + is OtherState.RoomTopic -> { + val hasRoomTopic = content.topic != null + when { + senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed_by_you, content.topic) + senderIsYou && !hasRoomTopic -> sp.getString(R.string.state_event_room_topic_removed_by_you) + !senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed, senderDisplayName, content.topic) + else -> sp.getString(R.string.state_event_room_topic_removed, senderDisplayName) + } + } + is OtherState.Custom -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "Custom event ${content.eventType}" + } + } + OtherState.PolicyRuleRoom -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "PolicyRuleRoom" + } + } + OtherState.PolicyRuleServer -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "PolicyRuleServer" + } + } + OtherState.PolicyRuleUser -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "PolicyRuleUser" + } + } + OtherState.RoomAliases -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "RoomAliases" + } + } + OtherState.RoomCanonicalAlias -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "RoomCanonicalAlias" + } + } + OtherState.RoomGuestAccess -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "RoomGuestAccess" + } + } + OtherState.RoomHistoryVisibility -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "RoomHistoryVisibility" + } + } + OtherState.RoomJoinRules -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "RoomJoinRules" + } + } + OtherState.RoomPinnedEvents -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "RoomPinnedEvents" + } + } + OtherState.RoomPowerLevels -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "RoomPowerLevels" + } + } + OtherState.RoomServerAcl -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "RoomServerAcl" + } + } + OtherState.RoomTombstone -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "RoomTombstone" + } + } + OtherState.SpaceChild -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "SpaceChild" + } + } + OtherState.SpaceParent -> when (renderingMode) { + RenderingMode.RoomList -> { + Timber.v("Filtering timeline item for room state change: $content") + null + } + RenderingMode.Timeline -> { + "SpaceParent" + } + } + } + } +} diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/mode/RenderingMode.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/mode/RenderingMode.kt new file mode 100644 index 0000000000..9f85dd4093 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/mode/RenderingMode.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.eventformatter.impl.mode + +enum class RenderingMode { + RoomList, + Timeline, +} 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 ace7572858..d6cc2e2e31 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 @@ -32,6 +32,9 @@ import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimesta import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider import io.element.android.libraries.designsystem.utils.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 +import io.element.android.libraries.eventformatter.impl.StateContentFormatter import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -53,10 +56,17 @@ class RoomListScreen( private val dateTimeProvider = LocalDateTimeProvider(clock, timeZone) private val dateFormatters = DateFormatters(locale, clock, timeZone) private val sessionVerificationService = matrixClient.sessionVerificationService() + private val stringProvider = AndroidStringProvider(context.resources) private val presenter = RoomListPresenter( client = matrixClient, lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters), - roomLastMessageFormatter = DefaultRoomLastMessageFormatter(AndroidStringProvider(context.resources), matrixClient), + roomLastMessageFormatter = DefaultRoomLastMessageFormatter( + sp = stringProvider, + matrixClient = matrixClient, + roomMembershipContentFormatter = RoomMembershipContentFormatter(matrixClient, stringProvider), + profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), + stateContentFormatter = StateContentFormatter(stringProvider), + ), sessionVerificationService = sessionVerificationService, networkMonitor = NetworkMonitorImpl(context), snackbarDispatcher = SnackbarDispatcher(), From 75f6c99ea95acbdf45dddb72e47ef82ed3873dab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 17 May 2023 11:21:14 +0200 Subject: [PATCH 19/34] Render State Event in the timeline. --- .../messages/impl/MessagesStateProvider.kt | 4 +- .../impl/timeline/TimelineStateProvider.kt | 24 +++-- .../messages/impl/timeline/TimelineView.kt | 68 +++++++++++-- .../timeline/components/MessageEventBubble.kt | 1 + .../components/MessageStateEventContainer.kt | 99 +++++++++++++++++++ .../event/TimelineItemContentView.kt | 5 + .../components/event/TimelineItemStateView.kt | 58 +++++++++++ .../event/TimelineItemEventContentProvider.kt | 4 + .../event/TimelineItemProfileChangeContent.kt | 5 +- .../TimelineItemRoomMembershipContent.kt | 5 +- .../model/event/TimelineItemStateContent.kt | 21 ++++ .../event/TimelineItemStateEventContent.kt | 5 +- 12 files changed, 266 insertions(+), 33 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStateView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateContent.kt 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 9249b808a1..beaba74a8c 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 @@ -20,9 +20,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.textcomposer.AttachmentSourcePicker import io.element.android.features.messages.impl.textcomposer.aMessageComposerState -import io.element.android.features.messages.impl.timeline.aTimelineItemContent import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineState +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.RoomId @@ -48,7 +48,7 @@ fun aMessagesState() = MessagesState( mode = MessageComposerMode.Normal("Hello"), ), timelineState = aTimelineState().copy( - timelineItems = aTimelineItemList(aTimelineItemContent()), + timelineItems = aTimelineItemList(aTimelineItemTextContent()), ), actionListState = anActionListState(), hasNetworkConnection = true, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index f6eaa55267..e119cb6e6c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -21,7 +21,8 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId @@ -55,6 +56,12 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList content = content, groupPosition = TimelineItemGroupPosition.First ), + // A state event on top of it + aTimelineItemEvent( + isMine = false, + content = aTimelineItemStateEventContent(), + groupPosition = TimelineItemGroupPosition.None + ), // 3 items (First Middle Last) with isMine = true aTimelineItemEvent( isMine = true, @@ -71,12 +78,18 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList content = content, groupPosition = TimelineItemGroupPosition.First ), + // A state event on top of it + aTimelineItemEvent( + isMine = true, + content = aTimelineItemStateEventContent(), + groupPosition = TimelineItemGroupPosition.None + ), ) } internal fun aTimelineItemEvent( isMine: Boolean = false, - content: TimelineItemEventContent = aTimelineItemContent(), + content: TimelineItemEventContent = aTimelineItemTextContent(), groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.First ): TimelineItem.Event { val randomId = "\$" + Random.nextInt().toString() @@ -96,10 +109,3 @@ internal fun aTimelineItemEvent( groupPosition = groupPosition, ) } - -internal fun aTimelineItemContent(): TimelineItemEventContent { - return TimelineItemTextContent( - body = "Text", - htmlDocument = null - ) -} 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 a378c508f6..954b1321f7 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 @@ -55,6 +55,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import io.element.android.features.messages.impl.timeline.components.MessageEventBubble +import io.element.android.features.messages.impl.timeline.components.MessageStateEventContainer import io.element.android.features.messages.impl.timeline.components.TimelineItemReactionsView import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView @@ -63,6 +64,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingModel import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -130,11 +132,12 @@ fun TimelineItemRow( onLongClick: (TimelineItem.Event) -> Unit, ) { when (timelineItem) { - is TimelineItem.Virtual -> TimelineItemVirtualRow( - virtual = timelineItem - ) + is TimelineItem.Virtual -> { + TimelineItemVirtualRow( + virtual = timelineItem + ) + } is TimelineItem.Event -> { - fun onClick() { onClick(timelineItem) } @@ -143,12 +146,21 @@ fun TimelineItemRow( onLongClick(timelineItem) } - TimelineItemEventRow( - event = timelineItem, - isHighlighted = isHighlighted, - onClick = ::onClick, - onLongClick = ::onLongClick - ) + if (timelineItem.content is TimelineItemStateContent) { + TimelineItemStateEventRow( + event = timelineItem, + isHighlighted = isHighlighted, + onClick = ::onClick, + onLongClick = ::onLongClick + ) + } else { + TimelineItemEventRow( + event = timelineItem, + isHighlighted = isHighlighted, + onClick = ::onClick, + onLongClick = ::onLongClick + ) + } } } } @@ -232,6 +244,42 @@ fun TimelineItemEventRow( } } +@Composable +fun TimelineItemStateEventRow( + event: TimelineItem.Event, + isHighlighted: Boolean, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier +) { + val interactionSource = remember { MutableInteractionSource() } + Box( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight(), + contentAlignment = Alignment.Center + ) { + MessageStateEventContainer( + isHighlighted = isHighlighted, + interactionSource = interactionSource, + onClick = onClick, + onLongClick = onLongClick, + modifier = Modifier + .zIndex(-1f) + .widthIn(max = 320.dp) + ) { + val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) + TimelineItemEventContentView( + content = event.content, + interactionSource = interactionSource, + onClick = onClick, + onLongClick = onLongClick, + modifier = contentModifier + ) + } + } +} + @Composable private fun MessageSenderInformation( sender: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt index b696b63f92..606d185568 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt @@ -84,6 +84,7 @@ fun MessageEventBubble( fun Modifier.offsetForItem(): Modifier { return if (state.isMine) { + // FIXME setting y offset to -12.dp can overlap a state event displayed above. offset(y = -(12.dp)) } else { offset(x = 20.dp, y = -(12.dp)) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt new file mode 100644 index 0000000000..fa1faaa93d --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt @@ -0,0 +1,99 @@ +/* + * 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.messages.impl.timeline.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Surface + +private val CORNER_RADIUS = 8.dp + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun MessageStateEventContainer( + isHighlighted: Boolean, + interactionSource: MutableInteractionSource, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + onLongClick: () -> Unit = {}, + content: @Composable () -> Unit = {}, +) { + val backgroundColor = if (isHighlighted) { + ElementTheme.colors.messageHighlightedBackground + } else { + Color.Companion.Transparent + } + val shape = RoundedCornerShape(CORNER_RADIUS) + Surface( + modifier = modifier + .widthIn(min = 80.dp) + .clip(shape) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + indication = rememberRipple(), + interactionSource = interactionSource + ), + color = backgroundColor, + shape = shape, + content = content + ) +} + +@Preview +@Composable +internal fun MessageStateEventContainerLightPreview() = + ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun MessageStateEventContainerDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + MessageStateEventContainer( + isHighlighted = false, + interactionSource = MutableInteractionSource(), + ) { + Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp)) + } + MessageStateEventContainer( + isHighlighted = true, + interactionSource = MutableInteractionSource(), + ) { + Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp)) + } + } +} 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/TimelineItemContentView.kt index bfb1d7f0c7..103b444cf9 100644 --- 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/TimelineItemContentView.kt @@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent @@ -58,5 +59,9 @@ fun TimelineItemEventContentView( content = content, modifier = modifier ) + is TimelineItemStateContent -> TimelineItemStateView( + content = content, + modifier = modifier + ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStateView.kt new file mode 100644 index 0000000000..1402d59fd6 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStateView.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components.event + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun TimelineItemStateView( + content: TimelineItemStateContent, + modifier: Modifier = Modifier +) { + Text( + modifier = modifier, + color = MaterialTheme.colorScheme.secondary, + fontSize = 13.sp, + text = content.body, + textAlign = TextAlign.Center, + ) +} + +@Preview +@Composable +internal fun TimelineItemStateViewLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun TimelineItemStateViewDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + TimelineItemStateView( + content = aTimelineItemStateEventContent(), + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt index 7e4be1bbd1..433c69088e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt @@ -65,3 +65,7 @@ fun aTimelineItemTextContent() = TimelineItemTextContent( ) fun aTimelineItemUnknownContent() = TimelineItemUnknownContent + +fun aTimelineItemStateEventContent() = TimelineItemStateEventContent( + body = "A state event", +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemProfileChangeContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemProfileChangeContent.kt index 29836dc28c..7d56394893 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemProfileChangeContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemProfileChangeContent.kt @@ -16,11 +16,8 @@ package io.element.android.features.messages.impl.timeline.model.event -import org.jsoup.nodes.Document - data class TimelineItemProfileChangeContent( override val body: String, - override val htmlDocument: Document? = null -) : TimelineItemTextBasedContent { +) : TimelineItemStateContent { override val type: String = "TimelineItemProfileChangeContent" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRoomMembershipContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRoomMembershipContent.kt index c18393b7a0..93607f01e6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRoomMembershipContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRoomMembershipContent.kt @@ -16,11 +16,8 @@ package io.element.android.features.messages.impl.timeline.model.event -import org.jsoup.nodes.Document - data class TimelineItemRoomMembershipContent( override val body: String, - override val htmlDocument: Document? = null -) : TimelineItemTextBasedContent { +) : TimelineItemStateContent { override val type: String = "TimelineItemRoomMembershipContent" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateContent.kt new file mode 100644 index 0000000000..b136a602b2 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateContent.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.messages.impl.timeline.model.event + +sealed interface TimelineItemStateContent : TimelineItemEventContent { + val body: String +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateEventContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateEventContent.kt index 8ac3e5a40b..1c656c9b96 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateEventContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStateEventContent.kt @@ -16,11 +16,8 @@ package io.element.android.features.messages.impl.timeline.model.event -import org.jsoup.nodes.Document - data class TimelineItemStateEventContent( override val body: String, - override val htmlDocument: Document? = null -) : TimelineItemTextBasedContent { +) : TimelineItemStateContent { override val type: String = "TimelineItemStateEventContent" } From 26198140dfd3f2c9436d48584fefaa9bea05e0c8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 17 May 2023 17:16:01 +0200 Subject: [PATCH 20/34] No actions (yet) for State Event. --- .../impl/actionlist/ActionListPresenter.kt | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index cdffa9c618..7eb3fbe433 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.rememberCoroutineScope import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.libraries.architecture.Presenter import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope @@ -53,20 +54,25 @@ class ActionListPresenter @Inject constructor() : Presenter { ) } - fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.Event, target: MutableState) = launch { + private fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.Event, target: MutableState) = launch { target.value = ActionListState.Target.Loading(timelineItem) val actions = - if (timelineItem.content is TimelineItemRedactedContent) { - emptyList() - } else { - mutableListOf( - TimelineItemAction.Reply, - TimelineItemAction.Forward, - TimelineItemAction.Copy, - ).also { - if (timelineItem.isMine) { - it.add(TimelineItemAction.Edit) - it.add(TimelineItemAction.Redact) + when (timelineItem.content) { + is TimelineItemRedactedContent, + is TimelineItemStateContent -> { + // TODO Add Share action (also) here, and developer options + emptyList() + } + else -> { + mutableListOf( + TimelineItemAction.Reply, + TimelineItemAction.Forward, + TimelineItemAction.Copy, + ).also { + if (timelineItem.isMine) { + it.add(TimelineItemAction.Edit) + it.add(TimelineItemAction.Redact) + } } } } From 0c95912c9cf0c4899eb1985198d8a2195007e5c7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 22 May 2023 13:53:06 +0200 Subject: [PATCH 21/34] Group some state events in the timeline --- .../src/main/res/values-de/translations.xml | 2 +- .../features/messages/impl/MessagesView.kt | 13 +- .../messages/impl/timeline/TimelineEvents.kt | 2 + .../impl/timeline/TimelinePresenter.kt | 13 +- .../messages/impl/timeline/TimelineView.kt | 45 +++++- .../components/group/GroupHeaderView.kt | 134 ++++++++++++++++++ .../timeline/groups/TimelineItemGrouper.kt | 82 +++++++++++ .../impl/timeline/model/TimelineItem.kt | 12 ++ .../src/main/res/values-es/translations.xml | 7 + .../src/main/res/values-it/translations.xml | 7 + .../src/main/res/values-ro/translations.xml | 8 ++ .../impl/src/main/res/values/localazy.xml | 4 + .../src/main/res/values-de/translations.xml | 9 +- .../src/main/res/values-es/translations.xml | 4 - .../src/main/res/values-it/translations.xml | 4 - .../src/main/res/values-ro/translations.xml | 5 - .../src/main/res/values/localazy.xml | 8 +- tools/localazy/config.json | 5 +- 18 files changed, 332 insertions(+), 32 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt create mode 100644 features/messages/impl/src/main/res/values-es/translations.xml create mode 100644 features/messages/impl/src/main/res/values-it/translations.xml create mode 100644 features/messages/impl/src/main/res/values-ro/translations.xml diff --git a/features/createroom/impl/src/main/res/values-de/translations.xml b/features/createroom/impl/src/main/res/values-de/translations.xml index 210f7f4cb4..6c3a7c8130 100644 --- a/features/createroom/impl/src/main/res/values-de/translations.xml +++ b/features/createroom/impl/src/main/res/values-de/translations.xml @@ -6,4 +6,4 @@ "Privater Raum (nur auf Einladung)" "Raumname" "Thema (optional)" - + \ No newline at end of file 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 a55ea1021d..c1708d3086 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 @@ -63,6 +63,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc import io.element.android.features.messages.impl.textcomposer.AttachmentSourcePicker import io.element.android.features.messages.impl.textcomposer.MessageComposerEvents import io.element.android.features.messages.impl.textcomposer.MessageComposerView +import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineView import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView @@ -139,6 +140,11 @@ fun MessagesView( } } + fun onExpandGroupClick(event: TimelineItem.GroupedEvents) { + Timber.v("onExpandGroupClick= ${event.id}") + state.timelineState.eventSink(TimelineEvents.ToggleExpandGroup(event)) + } + fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) { state.eventSink(MessagesEvents.HandleAction(action, event)) } @@ -189,7 +195,8 @@ fun MessagesView( .padding(padding) .consumeWindowInsets(padding), onMessageClicked = ::onMessageClicked, - onMessageLongClicked = ::onMessageLongClicked + onMessageLongClicked = ::onMessageLongClicked, + onExpandGroupClick = ::onExpandGroupClick, ) }, snackbarHost = { @@ -214,6 +221,7 @@ fun MessagesViewContent( modifier: Modifier = Modifier, onMessageClicked: (TimelineItem.Event) -> Unit = {}, onMessageLongClicked: (TimelineItem.Event) -> Unit = {}, + onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {}, ) { Column( modifier = modifier @@ -227,7 +235,8 @@ fun MessagesViewContent( state = state.timelineState, modifier = Modifier.weight(1f), onMessageClicked = onMessageClicked, - onMessageLongClicked = onMessageLongClicked + onMessageLongClicked = onMessageLongClicked, + onExpandGroupClick = onExpandGroupClick, ) } MessageComposerView( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt index ff64441198..11f1a1a483 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt @@ -16,9 +16,11 @@ package io.element.android.features.messages.impl.timeline +import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.matrix.api.core.EventId sealed interface TimelineEvents { object LoadMore : TimelineEvents data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents + data class ToggleExpandGroup(val event: TimelineItem.GroupedEvents) : TimelineEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 9418e59edc..1693a623f3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -17,15 +17,18 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory +import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.bool.orFalse 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.timeline.MatrixTimeline @@ -42,6 +45,7 @@ private const val backPaginationPageSize = 50 class TimelinePresenter @Inject constructor( private val timelineItemsFactory: TimelineItemsFactory, + private val timelineItemGrouper: TimelineItemGrouper, room: MatrixRoom, ) : Presenter { @@ -53,6 +57,8 @@ class TimelinePresenter @Inject constructor( val highlightedEventId: MutableState = rememberSaveable { mutableStateOf(null) } + val expandedGroups = remember { mutableStateMapOf() } + val timelineItems = timelineItemsFactory .flow() .collectAsState() @@ -65,6 +71,9 @@ class TimelinePresenter @Inject constructor( when (event) { TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState.value) is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId + is TimelineEvents.ToggleExpandGroup -> { + expandedGroups[event.event.identifier()] = expandedGroups[event.event.identifier()].orFalse().not() + } } } @@ -83,7 +92,7 @@ class TimelinePresenter @Inject constructor( return TimelineState( highlightedEventId = highlightedEventId.value, paginationState = paginationState.value, - timelineItems = timelineItems.value.toImmutableList(), + timelineItems = timelineItemGrouper.group(timelineItems.value, expandedGroups).toImmutableList(), eventSink = ::handleEvents ) } 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 954b1321f7..4b8a1b4869 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 @@ -50,14 +50,17 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.LastBaseline +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import io.element.android.features.messages.impl.R import io.element.android.features.messages.impl.timeline.components.MessageEventBubble import io.element.android.features.messages.impl.timeline.components.MessageStateEventContainer import io.element.android.features.messages.impl.timeline.components.TimelineItemReactionsView import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView +import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -84,6 +87,7 @@ fun TimelineView( modifier: Modifier = Modifier, onMessageClicked: (TimelineItem.Event) -> Unit = {}, onMessageLongClicked: (TimelineItem.Event) -> Unit = {}, + onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {}, ) { fun onReachedLoadMore() { @@ -106,9 +110,10 @@ fun TimelineView( ) { index, timelineItem -> TimelineItemRow( timelineItem = timelineItem, - isHighlighted = timelineItem.identifier() == state.highlightedEventId?.value, + highlightedItem = state.highlightedEventId?.value, onClick = onMessageClicked, - onLongClick = onMessageLongClicked + onLongClick = onMessageLongClicked, + onExpandGroupClick = onExpandGroupClick, ) if (index == state.timelineItems.lastIndex) { onReachedLoadMore() @@ -127,9 +132,10 @@ fun TimelineView( @Composable fun TimelineItemRow( timelineItem: TimelineItem, - isHighlighted: Boolean, + highlightedItem: String?, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, + onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit, ) { when (timelineItem) { is TimelineItem.Virtual -> { @@ -149,19 +155,48 @@ fun TimelineItemRow( if (timelineItem.content is TimelineItemStateContent) { TimelineItemStateEventRow( event = timelineItem, - isHighlighted = isHighlighted, + isHighlighted = highlightedItem == timelineItem.identifier(), onClick = ::onClick, onLongClick = ::onLongClick ) } else { TimelineItemEventRow( event = timelineItem, - isHighlighted = isHighlighted, + isHighlighted = highlightedItem == timelineItem.identifier(), onClick = ::onClick, onLongClick = ::onLongClick ) } } + is TimelineItem.GroupedEvents -> { + fun onExpandGroupClick() { + onExpandGroupClick(timelineItem) + } + + if (timelineItem.expanded) { + Column { + timelineItem.events.forEach { subGroupEvent -> + TimelineItemRow( + timelineItem = subGroupEvent, + highlightedItem = highlightedItem, + onClick = onClick, + onLongClick = onLongClick, + onExpandGroupClick = {} + ) + } + } + } + GroupHeaderView( + text = pluralStringResource( + id = R.plurals.room_timeline_state_changes, + count = timelineItem.events.size, + timelineItem.events.size + ), + isExpanded = timelineItem.expanded, + isHighlighted = !timelineItem.expanded && timelineItem.events.any { it.identifier() == highlightedItem }, + onClick = ::onExpandGroupClick, + ) + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt new file mode 100644 index 0000000000..d952d573f9 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt @@ -0,0 +1,134 @@ +/* + * 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.messages.impl.timeline.components.group + +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.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowRight +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text + +private val CORNER_RADIUS = 8.dp + +@Composable +fun GroupHeaderView( + text: String, + isExpanded: Boolean, + isHighlighted: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val backgroundColor = if (isHighlighted) { + ElementTheme.colors.messageHighlightedBackground + } else { + Color.Companion.Transparent + } + val shape = RoundedCornerShape(CORNER_RADIUS) + + Box( + modifier = modifier + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Surface( + modifier = Modifier + .clip(shape) + .clickable(onClick = onClick), + color = backgroundColor, + shape = shape, + ) { + Row( + modifier = Modifier + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = text, + color = MaterialTheme.colorScheme.secondary, + fontSize = 13.sp + ) + val icon = if (isExpanded) { + Icons.Default.ArrowDropDown + } else { + Icons.Default.ArrowRight + } + Icon(icon, "", tint = MaterialTheme.colorScheme.secondary) + } + } + } +} + +@Preview +@Composable +fun GroupHeaderViewLightPreview() = + ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun GroupHeaderViewDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + GroupHeaderView( + text = "8 room changes (expanded)", + isExpanded = true, + isHighlighted = false, + onClick = {} + ) + GroupHeaderView( + text = "8 room changes (not expanded)", + isExpanded = false, + isHighlighted = false, + onClick = {} + ) + GroupHeaderView( + text = "8 room changes (expanded/h)", + isExpanded = true, + isHighlighted = true, + onClick = {} + ) + GroupHeaderView( + text = "8 room changes (not expanded/h)", + isExpanded = false, + isHighlighted = true, + onClick = {} + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt new file mode 100644 index 0000000000..1ec382a4e5 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.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.features.messages.impl.timeline.groups + +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent +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.libraries.core.bool.orFalse +import kotlinx.collections.immutable.toImmutableList + +import javax.inject.Inject + +/** + * Create a new list of [TimelineItem] by grouping some of them into [TimelineItem.GroupedEvents]. + */ +class TimelineItemGrouper @Inject constructor() { + fun group(from: List, expandedGroups: Map): List { + val result = mutableListOf() + val currentGroup = mutableListOf() + from.forEach { timelineItem -> + if (timelineItem is TimelineItem.Event && timelineItem.canBeGrouped()) { + currentGroup.add(0, timelineItem) + } else { + // timelineItem cannot be grouped + if (currentGroup.isNotEmpty()) { + // There is a pending group, create a TimelineItem.GroupedEvents if there is more than 1 Event in the pending group. + if (currentGroup.size == 1) { + // Do not create a group with just 1 item, just add the item to the result + result.add(currentGroup.first()) + } else { + result.add( + TimelineItem.GroupedEvents( + expanded = expandedGroups[currentGroup.first().id + "_group"].orFalse(), + events = currentGroup.toImmutableList() + ) + ) + } + currentGroup.clear() + } + result.add(timelineItem) + } + } + return result + } + + private fun TimelineItem.Event.canBeGrouped(): Boolean { + return when (content) { + is TimelineItemEncryptedContent, + is TimelineItemImageContent, + TimelineItemRedactedContent, + is TimelineItemEmoteContent, + is TimelineItemNoticeContent, + is TimelineItemTextContent, + TimelineItemUnknownContent -> false + is TimelineItemProfileChangeContent, + is TimelineItemRoomMembershipContent, + is TimelineItemStateEventContent -> true + } + } +} 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 e1b6557f31..fe34cd5b1d 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 @@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.collections.immutable.ImmutableList @Immutable sealed interface TimelineItem { @@ -29,11 +30,13 @@ sealed interface TimelineItem { fun identifier(): String = when (this) { is Event -> id is Virtual -> id + is GroupedEvents -> id } fun contentType(): String = when (this) { is Event -> content.type is Virtual -> model.type + is GroupedEvents -> "groupedEvent" } @Immutable @@ -60,4 +63,13 @@ sealed interface TimelineItem { val safeSenderName: String = senderDisplayName ?: senderId.value } + + @Immutable + data class GroupedEvents( + val expanded: Boolean, + val events: ImmutableList, + ) : TimelineItem { + // use first id with a suffix + val id = events.first().id + "_group" + } } diff --git a/features/messages/impl/src/main/res/values-es/translations.xml b/features/messages/impl/src/main/res/values-es/translations.xml new file mode 100644 index 0000000000..7cd4b6e764 --- /dev/null +++ b/features/messages/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,7 @@ + + + + "%1$d cambio en la sala" + "%1$d cambios en la sala" + + \ No newline at end of file diff --git a/features/messages/impl/src/main/res/values-it/translations.xml b/features/messages/impl/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..649a91405b --- /dev/null +++ b/features/messages/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,7 @@ + + + + "%1$d modifica alla stanza" + "%1$d modifiche alla stanza" + + \ No newline at end of file diff --git a/features/messages/impl/src/main/res/values-ro/translations.xml b/features/messages/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..68d83cacfe --- /dev/null +++ b/features/messages/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,8 @@ + + + + "%1$d schimbare a camerii" + "%1$d schimbări ale camerei" + "%1$d schimbări ale camerei" + + \ No newline at end of file diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 8c53bca6c4..361e80a4de 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -1,5 +1,9 @@ + + "%1$d room change" + "%1$d room changes" + "Camera" "Take photo" "Record a video" 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 428d42ec14..2fb21016e5 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -26,6 +26,7 @@ "Raum verlassen" "Weiter" "Nein" + "Nicht jetzt" "OK" "Schnellantwort" "Zitieren" @@ -41,10 +42,11 @@ "Teilen" "Link teilen" "Überspringen" + "Chat starten" "Foto aufnehmen" "Ja" "Über" - "Analytik" + "Analyse" "Audio" "Blasen" "Entschlüsselungsfehler" @@ -61,6 +63,7 @@ "Offline" "Passwort" "Reaktionen" + "Fehler melden" "Suchergebnisse" "Sicherheit" "Server wird nicht unterstützt" @@ -87,6 +90,7 @@ "Reisen & Orte" "Symbole" "Fehler beim Laden der Nachrichten" + "Einige Nachrichten wurden nicht gesendet" "Entschuldigung, ein Fehler ist aufgetreten." "%1$s Android" @@ -96,6 +100,9 @@ "Grund für die Meldung dieses Inhalts" "Dies ist der Anfang von %1$s." "Neu" + "Wir erfassen und analysieren ""keine"" Account-Daten" + "Sie können die Analyse jederzeit in den Einstellungen deaktivieren" + "Wir geben ""keine"" Informationen an Dritte weiter" "Teile Analyse-Daten" "Medienauswahl fehlgeschlagen, bitte versuche es erneut." "Erkennungsschwelle" diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index 78a386f650..b1e73503fa 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -116,10 +116,6 @@ "%1$d miembro" "%1$d miembros" - - "%1$d cambio en la sala" - "%1$d cambios en la sala" - "Agitar con fuerza para informar de un error" "Parece que sacudes el teléfono con frustración. ¿Quieres abrir la pantalla de informe de errores?" "Este mensaje se notificará al administrador de su homeserver. No podrán leer ningún mensaje cifrado." diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index 20f255a21c..c8b16a7ea5 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -116,10 +116,6 @@ "%1$d membro" "%1$d membri" - - "%1$d modifica alla stanza" - "%1$d modifiche alla stanza" - "Scuoti per segnalare un problema" "Sembra che tu stia scuotendo il telefono per la frustrazione. Vuoi aprire la schermata di segnalazione dei problemi?" "Questo messaggio verrà segnalato all\'amministratore dell\'homeserver. Questi non sarà in grado di leggere i messaggi criptati." diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index d7b052badc..efcd69b3fc 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -125,11 +125,6 @@ "%1$d membri" "%1$d membri" - - "%1$d schimbare a camerii" - "%1$d schimbări ale camerei" - "%1$d schimbări ale camerei" - "Rageshake pentru a raporta erori" "Se pare că scuturați telefonul de frustrare. Doriți să deschdeți ecranul de raportare a unei erori?" "Acest mesaj va fi raportat administratorilor homeserver-ului tau. Ei nu vor putea citi niciun mesaj criptat." diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index ca2bcd1d28..163d65f19f 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -122,7 +122,7 @@ "Failed loading messages" "Some messages have not been sent" "Sorry, an error occurred" - "🔐️ Join me on %1$s" + "🔐️ Join me on %1$s" "Hey, talk to me on %1$s: %2$s" "Are you sure that you want to leave this room? You are the only person here. If you leave, no one will be able to join in the future, including you." "Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite." @@ -132,10 +132,6 @@ "%1$d member" "%1$d members" - - "%1$d room change" - "%1$d room changes" - "Rageshake to report bug" "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?" "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages." @@ -167,4 +163,4 @@ "You can read all our terms %1$s." "here" "Block user" - \ No newline at end of file + diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 1e0e125137..ef7110a160 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -57,7 +57,7 @@ ] }, { - "name": ":libraries:messageformatter:impl", + "name": ":libraries:eventformatter:impl", "includeRegex": [ "state_event_.*" ] @@ -95,7 +95,8 @@ "name": ":features:messages:impl", "includeRegex": [ "screen_room_.*", - "screen_dm_details_.*" + "screen_dm_details_.*", + "room_timeline_state_changes" ], "excludeRegex": [ "screen_room_details_.*", From 4b9a01d7537cb2a0e32ca48e44ae3221de5a1c6b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 22 May 2023 15:26:30 +0200 Subject: [PATCH 22/34] Record screenshots --- ...Group_GroupHeaderViewDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...roup_GroupHeaderViewLightPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2cd38edf69 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7aa84af08d14990c5a28f0fff6ec28cec7881393c4e2116ec50a2bffb3c647a +size 53307 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8a2b055cca --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7609a858f1d08750e0007c27b1855cb3eddc623ebde17e66d3f8175d5f68562b +size 52390 From 31ec3267db8e7c948e1628ea756e89b22f93800d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 22 May 2023 17:21:16 +0200 Subject: [PATCH 23/34] Animate collapse/expand change --- .../messages/impl/timeline/TimelineView.kt | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) 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 4b8a1b4869..7c607ca8d4 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 @@ -16,6 +16,7 @@ package io.element.android.features.messages.impl.timeline +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -173,29 +174,31 @@ fun TimelineItemRow( onExpandGroupClick(timelineItem) } - if (timelineItem.expanded) { - Column { - timelineItem.events.forEach { subGroupEvent -> - TimelineItemRow( - timelineItem = subGroupEvent, - highlightedItem = highlightedItem, - onClick = onClick, - onLongClick = onLongClick, - onExpandGroupClick = {} - ) + Column(modifier = Modifier.animateContentSize()) { + GroupHeaderView( + text = pluralStringResource( + id = R.plurals.room_timeline_state_changes, + count = timelineItem.events.size, + timelineItem.events.size + ), + isExpanded = timelineItem.expanded, + isHighlighted = !timelineItem.expanded && timelineItem.events.any { it.identifier() == highlightedItem }, + onClick = ::onExpandGroupClick, + ) + if (timelineItem.expanded) { + Column { + timelineItem.events.forEach { subGroupEvent -> + TimelineItemRow( + timelineItem = subGroupEvent, + highlightedItem = highlightedItem, + onClick = onClick, + onLongClick = onLongClick, + onExpandGroupClick = {} + ) + } } } } - GroupHeaderView( - text = pluralStringResource( - id = R.plurals.room_timeline_state_changes, - count = timelineItem.events.size, - timelineItem.events.size - ), - isExpanded = timelineItem.expanded, - isHighlighted = !timelineItem.expanded && timelineItem.events.any { it.identifier() == highlightedItem }, - onClick = ::onExpandGroupClick, - ) } } } From 52d35e0ac43be407d55295e218f9ad2e083d307e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 22 May 2023 17:23:53 +0200 Subject: [PATCH 24/34] Better icon for this use case. --- .../impl/timeline/components/group/GroupHeaderView.kt | 8 ++++---- ..._GroupHeaderViewDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...GroupHeaderViewLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt index d952d573f9..e747134405 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt @@ -25,8 +25,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.ArrowRight +import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -83,9 +83,9 @@ fun GroupHeaderView( fontSize = 13.sp ) val icon = if (isExpanded) { - Icons.Default.ArrowDropDown + Icons.Default.ExpandLess } else { - Icons.Default.ArrowRight + Icons.Default.ExpandMore } Icon(icon, "", tint = MaterialTheme.colorScheme.secondary) } diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewDarkPreview_0_null,NEXUS_5,1.0,en].png index 2cd38edf69..fc37edc2ef 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7aa84af08d14990c5a28f0fff6ec28cec7881393c4e2116ec50a2bffb3c647a -size 53307 +oid sha256:a1846b6a8267752a502ded5cfba052569c929fbb7b53748885b6aba43638b745 +size 54262 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewLightPreview_0_null,NEXUS_5,1.0,en].png index 8a2b055cca..f638023723 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.group_null_DefaultGroup_GroupHeaderViewLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7609a858f1d08750e0007c27b1855cb3eddc623ebde17e66d3f8175d5f68562b -size 52390 +oid sha256:6c90a40ad84d4ee108ddbd94fb7dfb9de0203f792c51409565b146f26fcf3763 +size 53225 From 4c20450c2a3b5f6c803eaefe518c6ee0d9592cbf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 22 May 2023 17:27:31 +0200 Subject: [PATCH 25/34] This is default value when `reverseLayout = true` --- .../android/features/messages/impl/timeline/TimelineView.kt | 3 --- 1 file changed, 3 deletions(-) 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 7c607ca8d4..c9168fd369 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 @@ -18,7 +18,6 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.animation.animateContentSize import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column @@ -100,8 +99,6 @@ fun TimelineView( LazyColumn( modifier = Modifier.fillMaxSize(), state = lazyListState, - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Bottom, reverseLayout = true ) { itemsIndexed( From 9efb703854b9add31724105241c8a860e9502c36 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 May 2023 11:26:02 +0200 Subject: [PATCH 26/34] Fix test compilation --- .../timeline/groups/TimelineItemGrouper.kt | 6 +-- .../messages/MessagesPresenterTest.kt | 2 + .../messages/fixtures/timelineItemsFactory.kt | 51 ++++++++++++------- .../MessageComposerPresenterTest.kt | 3 ++ .../timeline/TimelinePresenterTest.kt | 4 ++ .../DefaultRoomLastMessageFormatterTests.kt | 9 +++- 6 files changed, 52 insertions(+), 23 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt index 1ec382a4e5..622400ee93 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt @@ -32,10 +32,10 @@ import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject -/** - * Create a new list of [TimelineItem] by grouping some of them into [TimelineItem.GroupedEvents]. - */ class TimelineItemGrouper @Inject constructor() { + /** + * Create a new list of [TimelineItem] by grouping some of them into [TimelineItem.GroupedEvents]. + */ fun group(from: List, expandedGroups: Map): List { val result = mutableListOf() val currentGroup = mutableListOf() 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 e6ee61ee52..f01c8581ab 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 @@ -28,6 +28,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter +import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.featureflag.test.FakeFeatureFlagService @@ -138,6 +139,7 @@ class MessagesPresenterTest { ) val timelinePresenter = TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), + timelineItemGrouper = TimelineItemGrouper(), room = matrixRoom, ) val actionListPresenter = ActionListPresenter() diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt index 058406d513..5d355be8d0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt @@ -31,26 +31,39 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemDaySeparatorFactory import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter +import io.element.android.libraries.eventformatter.api.TimelineEventFormatter +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.tests.testutils.testCoroutineDispatchers -internal fun aTimelineItemsFactory() = TimelineItemsFactory( - dispatchers = testCoroutineDispatchers(), - eventItemFactory = TimelineItemEventFactory( - TimelineItemContentFactory( - messageFactory = TimelineItemContentMessageFactory(), - redactedMessageFactory = TimelineItemContentRedactedFactory(), - stickerFactory = TimelineItemContentStickerFactory(), - utdFactory = TimelineItemContentUTDFactory(), - roomMembershipFactory = TimelineItemContentRoomMembershipFactory(), - profileChangeFactory = TimelineItemContentProfileChangeFactory(), - stateFactory = TimelineItemContentStateFactory(), - failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(), - failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory() - ) - ), - virtualItemFactory = TimelineItemVirtualFactory( - daySeparatorFactory = TimelineItemDaySeparatorFactory( - FakeDaySeparatorFormatter() +internal fun aTimelineItemsFactory(): TimelineItemsFactory { + val timelineEventFormatter = aTimelineEventFormatter() + return TimelineItemsFactory( + dispatchers = testCoroutineDispatchers(), + eventItemFactory = TimelineItemEventFactory( + TimelineItemContentFactory( + messageFactory = TimelineItemContentMessageFactory(), + redactedMessageFactory = TimelineItemContentRedactedFactory(), + stickerFactory = TimelineItemContentStickerFactory(), + utdFactory = TimelineItemContentUTDFactory(), + roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter), + profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter), + stateFactory = TimelineItemContentStateFactory(timelineEventFormatter), + failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(), + failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory() + ) ), + virtualItemFactory = TimelineItemVirtualFactory( + daySeparatorFactory = TimelineItemDaySeparatorFactory( + FakeDaySeparatorFormatter() + ), + ) ) -) +} + +internal fun aTimelineEventFormatter(): TimelineEventFormatter { + return object : TimelineEventFormatter { + override fun format(event: EventTimelineItem): CharSequence { + return "" + } + } +} 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 9639759529..0c0b46cbe2 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 @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.messages.textcomposer import app.cash.molecule.RecompositionClock @@ -50,6 +52,7 @@ import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.textcomposer.MessageComposerMode import io.mockk.mockk import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index a88b2251a4..e8fe681692 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -23,6 +23,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter +import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import kotlinx.coroutines.test.runTest @@ -33,6 +34,7 @@ class TimelinePresenterTest { fun `present - initial state`() = runTest { val presenter = TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), + timelineItemGrouper = TimelineItemGrouper(), room = FakeMatrixRoom(), ) moleculeFlow(RecompositionClock.Immediate) { @@ -49,6 +51,7 @@ class TimelinePresenterTest { fun `present - load more`() = runTest { val presenter = TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), + timelineItemGrouper = TimelineItemGrouper(), room = FakeMatrixRoom(), ) moleculeFlow(RecompositionClock.Immediate) { @@ -71,6 +74,7 @@ class TimelinePresenterTest { fun `present - set highlighted event`() = runTest { val presenter = TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), + timelineItemGrouper = TimelineItemGrouper(), room = FakeMatrixRoom(), ) moleculeFlow(RecompositionClock.Immediate) { 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 3016ff4884..6e8eb3d81c 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 @@ -67,7 +67,14 @@ class DefaultRoomLastMessageFormatterTests { fun setup() { context = RuntimeEnvironment.getApplication() as Context fakeMatrixClient = FakeMatrixClient() - formatter = DefaultRoomLastMessageFormatter(AndroidStringProvider(context.resources), fakeMatrixClient) + val stringProvider = AndroidStringProvider(context.resources) + formatter = DefaultRoomLastMessageFormatter( + sp = AndroidStringProvider(context.resources), + matrixClient = fakeMatrixClient, + roomMembershipContentFormatter = RoomMembershipContentFormatter(fakeMatrixClient, stringProvider), + profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), + stateContentFormatter = StateContentFormatter(stringProvider) + ) } @Test From 78c99a340ea7172aaad892e4ec90d8bbaa71f859 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 May 2023 11:30:13 +0200 Subject: [PATCH 27/34] Fix missing modifier parameter. --- .../features/messages/impl/timeline/TimelineView.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 c9168fd369..4478ddb9eb 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 @@ -134,11 +134,13 @@ fun TimelineItemRow( onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit, + modifier: Modifier = Modifier ) { when (timelineItem) { is TimelineItem.Virtual -> { TimelineItemVirtualRow( - virtual = timelineItem + virtual = timelineItem, + modifier = modifier, ) } is TimelineItem.Event -> { @@ -155,14 +157,16 @@ fun TimelineItemRow( event = timelineItem, isHighlighted = highlightedItem == timelineItem.identifier(), onClick = ::onClick, - onLongClick = ::onLongClick + onLongClick = ::onLongClick, + modifier = modifier, ) } else { TimelineItemEventRow( event = timelineItem, isHighlighted = highlightedItem == timelineItem.identifier(), onClick = ::onClick, - onLongClick = ::onLongClick + onLongClick = ::onLongClick, + modifier = modifier, ) } } @@ -171,7 +175,7 @@ fun TimelineItemRow( onExpandGroupClick(timelineItem) } - Column(modifier = Modifier.animateContentSize()) { + Column(modifier = modifier.animateContentSize()) { GroupHeaderView( text = pluralStringResource( id = R.plurals.room_timeline_state_changes, From f36b5d62e0ce16eaf022fc8ba61bf3d51b9ba772 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 May 2023 14:39:39 +0200 Subject: [PATCH 28/34] Record screenshot --- ..._DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...elineItemImageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...lineItemImageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...imelineItemStateViewDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...melineItemStateViewLightPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...eStateEventContainerDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...StateEventContainerLightPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...agesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...agesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...agesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...agesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...gesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...gesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...gesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...gesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...neItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...oup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...up_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...roup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 2 +- ...oup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- 42 files changed, 87 insertions(+), 75 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemStateViewDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemStateViewLightPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageStateEventContainerDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageStateEventContainerLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png index efe83873c0..a7e6d29b5f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7787897d8dda469f17da7151260f26b6dcc5500f93ce44fb317af42ae8d457c -size 93739 +oid sha256:cbfbfa08085539bbfa4f20bc5d149bf0db0036169a32747c96f956d0d0f4b42d +size 93741 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png index 6dae242f3c..e278779a22 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6691512398d2c7e319d15397120054f1c2aae452ffed0be47df72ecb906ac4fa -size 93675 +oid sha256:9e0ba60e784a0dc582826a3f5679a6015531dc9aca22617ec1e0dede38b97f30 +size 93678 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 964891e6d2..ca85a9b25c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44313efc4d87857b9eb0bb05c8d24330b17353fd95344bcce06c1ad1bba8df81 -size 492167 +oid sha256:766b2d44b51a36d1b15d8a39434ce5237f8427d1150f9b4c1255043fc1e4bb79 +size 492165 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 964891e6d2..ca85a9b25c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44313efc4d87857b9eb0bb05c8d24330b17353fd95344bcce06c1ad1bba8df81 -size 492167 +oid sha256:766b2d44b51a36d1b15d8a39434ce5237f8427d1150f9b4c1255043fc1e4bb79 +size 492165 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemStateViewDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemStateViewDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ea5deaa132 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemStateViewDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a94a376c6b7f7db5e901e23cd9ba4858bdd47fa330161b4d30daa977efda048 +size 3078 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemStateViewLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemStateViewLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a076d976ed --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemStateViewLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:977ad7144c5d2cb5d6eec5bcfbedf81bc39a83e9fcd0243aab06252d9090640b +size 3094 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageStateEventContainerDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageStateEventContainerDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5fb8afab7b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageStateEventContainerDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d7d5bc725daafd716308b8f5415a8357608941992c1f1133300aaccb81969ac +size 1582 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageStateEventContainerLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageStateEventContainerLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a8662beb70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageStateEventContainerLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dae0c56765a6004b43f904e652b1605294eb688c7b3d72af4385f2c58a8234eb +size 1651 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 17e1d2852a..7d93e5c715 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:523d6607d70a331d2ec32aeabf033d59c4dc60e0e7373982893df0245ae924ce -size 3332 +oid sha256:a3443f60b8ceec7eccfee92c1bca8cad151e25cff8e7845459a5f4c3d50e5e08 +size 3330 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png index abfa1dae68..7b025a402e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:266490a1bdce29ac65d875543deda072d7f2a9aa2e216efb0d54b93c2a11aa5f -size 4001 +oid sha256:d0b0cdb7e39d5ca5ce874477da4397ebed00ecf9f5d8980f9f3b03ab72259d9f +size 4000 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 17e1d2852a..7d93e5c715 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:523d6607d70a331d2ec32aeabf033d59c4dc60e0e7373982893df0245ae924ce -size 3332 +oid sha256:a3443f60b8ceec7eccfee92c1bca8cad151e25cff8e7845459a5f4c3d50e5e08 +size 3330 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png index abfa1dae68..7b025a402e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:266490a1bdce29ac65d875543deda072d7f2a9aa2e216efb0d54b93c2a11aa5f -size 4001 +oid sha256:d0b0cdb7e39d5ca5ce874477da4397ebed00ecf9f5d8980f9f3b03ab72259d9f +size 4000 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png index 66a540e8a7..eb0f1bcfde 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e94515b43bc4b8427edc60456ec657ec38b33539a034a2c45c92624a6b75d4e8 -size 3138 +oid sha256:d35eb1aac0d413c216dc77f45f655be97ae9d36c3cade57ee8f70e25c44fafd1 +size 3142 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png index 6a087cd16c..69b558a480 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4dc928a04bfc6331673be2fa0bf7f12e384dfd8ff91b07dc57b220c8dbbdd71 -size 3829 +oid sha256:782a439312e4b616a6dd3020d8c99d04042208b2ebc416fab73c95344adeba67 +size 3834 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png index 66a540e8a7..eb0f1bcfde 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e94515b43bc4b8427edc60456ec657ec38b33539a034a2c45c92624a6b75d4e8 -size 3138 +oid sha256:d35eb1aac0d413c216dc77f45f655be97ae9d36c3cade57ee8f70e25c44fafd1 +size 3142 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png index 6a087cd16c..69b558a480 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4dc928a04bfc6331673be2fa0bf7f12e384dfd8ff91b07dc57b220c8dbbdd71 -size 3829 +oid sha256:782a439312e4b616a6dd3020d8c99d04042208b2ebc416fab73c95344adeba67 +size 3834 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png index 89034f66b9..d6eab0d361 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25d0334932490997d432115c550c42e74e7226399ef82031430bf86ba33972bd -size 6066 +oid sha256:6cab21ee595c96700e4577554f9c98fdc56a59bf5093aa9d81a54d10381ba122 +size 6065 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png index c407012dfd..70722aa53f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2d15387248308e5234c84b9b2b282e736f05f0495c80807bacfb0acff31eb2b -size 5890 +oid sha256:b9235621126fe90a51d557df616878d53120dada814830fba006b54f1556e69e +size 5895 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 3fc91b65e4..84913d3fb7 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5315b975f8b9def0b0d7575160cb34300511f1b83845ee2d1762188481cf6fe9 -size 30927 +oid sha256:b625f841a45111d58d76fd290cfad376a58e16265cc0b0d8e00318ffa1e3f48e +size 35204 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 7a61ecf737..66225f1c42 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:099401d03621aca301d3c4590d90ba4e4642d400d7603a8e15140a1bfef0c0ec -size 42758 +oid sha256:a5391a08d36198d28cc2c05c83757d30dc207dd56ada9684cd3a3ea7f9812b0a +size 46977 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index e361e4a2d2..776728bf2a 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca5dbc3aa00d6116de38fc0217b20ab0db4096b1f76bcc18b43ab9fd343f8110 -size 32722 +oid sha256:6f951f69dd21d36338ddbfb303d50c554ef915c40e57919f699d83afce507d75 +size 36906 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png index 42f22c9a26..e5a622f671 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6211ec07d10d48e4713206e4c1fe24eae890fe2310d5bcc1f4ca54cc57f84ec9 -size 44564 +oid sha256:4dec759835366e66beae953d11e73a52717500b30d5c58df6e3912a3cc60f916 +size 48704 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png index b974aec366..328ffb4a69 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ac7985e70782a84fc351cb15536bfd700ca8d6d48d5da0ee9dcaf5c78226479 -size 28909 +oid sha256:3bebe664eb8cd7a0578977ad998ddfe24283a309a6830cc420416c99b00f1e60 +size 33114 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png index cf23661de1..af164ce007 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c99138940ed1a8beac7da3c673600016a03b87195e07f205d5d389ef05be0fb0 -size 45978 +oid sha256:9ed02fce151bfb9eec7b380e9469b67c8ce9757eb7a0b7ebbcf0a5932c55087e +size 50072 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 20f62bf8e6..4113c9a338 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f24112ad89e5ef93917d45e3232043168d5092ce6d62a7d414773dd0afda963 -size 30326 +oid sha256:73de3dd7281ea437f3cb364cca175cec5379046da19fd8f1152f3a52f43b6bfa +size 34442 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 534402d370..2781e8027e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c696c464515873a94d28f8909dc47255717fc3b012ce0d80000769714e3cb9c -size 42427 +oid sha256:4bb36a52495fc17de86c0b66c6ac5eb8d4df64e3c512db41ec546b02c8157133 +size 46507 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 88014ea46b..17545e89f6 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fe506e4e7adbfe19fd48f0a40c19343471e0bb06e12fac8d5e6d54595e7193a -size 32076 +oid sha256:b668eb7b9ce26c8d4b25d9e955ded21ef40c9db496a81cca99f299c84838fcd3 +size 36143 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png index e70fa9d639..08e023fa8d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9c9374c303c9a71b0526fccfd18903d145c1b3b0059de3ccd41cf80f1541a80 -size 44427 +oid sha256:3ce9ae983222355419305a239dae4deefa36cf90b03840b69401a3e9329ad4df +size 48421 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png index 4ebc61d047..5404d4ef7e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f18d8b33eace857f78b92eed035621125bbfca1f0d43e5e47fd1c3965324de0 -size 28319 +oid sha256:7692c7dd3ce432abda585d3e539ab03eb51f1df7debca4b14ba015cfd0111e42 +size 32372 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png index 336334e955..57c5b61d46 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be0722a804c2fc264f931f8784635bceab7ddcf08e378bed8ea510bd76813fbf -size 45642 +oid sha256:ffccef423c7be0a6bcb15facabb174de1f20833aa73af0db7397dd3eb011642e +size 49791 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 1ba319f8ee..27584b85d5 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b77478183675fd3bb9a7ef34ef808e3ad75b504a0236b4dcb0d96a7faafc29cc -size 39629 +oid sha256:71ead784370bd7007dc52c48d31651512721c42fdcd37a177cb078802848412d +size 43813 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 27b4e07cb9..c326a2eafa 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2645c93a097d35725a7afb83c97dd0ea6b5623990cbae2372aaf66c9011e353c -size 41506 +oid sha256:e29a3b27b84b1be439b6b80e2a6df9ef30c87d74b51ca1079c683fd8d935d2c1 +size 44308 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index dee6d7dd1a..c74ec030c4 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:394ffe0b730e7d4010ec0d7f96b5487b2775e70aa7e42b6bf8b2535ad5acc244 -size 37524 +oid sha256:78061b0dccb2cd76d0f124eb62f55db3bae1c05b533acadc76898669daa483db +size 41497 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png index 4aa436dc8c..e24e462642 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b87be006b0125029391e74a09bc2a07a906c33d9ea21ca17f884f2d8862af32a -size 36302 +oid sha256:9f535800616d0b9a03fd00088a823bc58b359c0c20c311816de824279308fee3 +size 40267 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index c3e23147ca..fee4e7edc4 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93f8dd057f245d454a6ebf81607c879b6ae4c20287a64d001bee36764e0802a5 -size 38657 +oid sha256:6a733f5480087262e0e4a51d528ebe27941c6b38452ea7d2412ab44306ce4664 +size 42709 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index c3d01356c6..a0f6bfb231 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a4ce62d7f8f27128fd59cd3e7eea068a002d7e2c0a054fef1f555d06cf91ebe -size 40708 +oid sha256:5dd805be3de3c5a22038e95bb0d2c9716b27a2af0156cfdb363c9cab0ba1bc8e +size 43460 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 4660650dfb..551258be5c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bee8752a24938844cae76aadae9148a19001302a9e4909cf69a192a98d583ba4 -size 36230 +oid sha256:896760a67355f8d7d332edd9e705d305503da7f96ed895a7a792d008e6352aea +size 40210 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png index 6e04daf76b..75c9b207a1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f89eebcabe514594c53e6e436ae63685e6b693628f2acf796c6306298aff1a8 -size 35199 +oid sha256:b5bcc73a77a86083a2229b174122ec76d0fdf07973f838c26862e9ece4990311 +size 39133 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png index b1f708d57c..9749477ebd 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b53d55b5085673ac3a0a7663f7ff68e7d510fe352f3931b49c7e75e4c3767b93 -size 60007 +oid sha256:c820bd324df729db08710d1a6c17ca34a451a3bb94da2206fc57d6b4efe91e2f +size 60019 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png index d020b1e521..f24edcc4a1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a761318f3dfbc2ce6e777cfabc19eeb2f89100ae7631660f4b4d7550cd947c84 -size 57580 +oid sha256:da2f9f87d89382b4b18d39a477a8e86f98067e2326adae4def8374b5bf09f316 +size 57588 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 01ec8ec0fc..07451a0330 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:147786f2cfcf7674147253cca5e41b4af96587a27216076a1c0802c81b0b1b46 +oid sha256:628f1f00dca9d15faabd8288a3c54b1b8581a380cf14edf364f0dee9ecc187d5 size 180117 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index a49d11835b..8146c241cb 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:789a979f7207531b4b9ba488ae2de52e6046809e788baf6690e1383a933cf624 -size 178916 +oid sha256:827bccb0fdd3106fb24a95d665b4da2cfc15de48e8f508ae809c9f75d6d1bbf0 +size 178915 From 2898d830d8729d65a675b2c9cfc375a2fd37bce1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 May 2023 15:04:01 +0200 Subject: [PATCH 29/34] Fix warning "Use version catalog instead" --- app/build.gradle.kts | 3 +-- gradle/libs.versions.toml | 2 ++ samples/minimal/build.gradle.kts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1909d9fa0f..d4e889adc1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -213,8 +213,7 @@ dependencies { implementation(projects.appnav) anvil(projects.anvilcodegen) - // https://developer.android.com/studio/write/java8-support#library-desugaring-versions - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") + coreLibraryDesugaring(libs.android.desugar) implementation(libs.appyx.core) implementation(libs.androidx.splash) implementation(libs.androidx.core) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b32859cca1..9f6b9c7d63 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,6 +53,8 @@ dependencygraph = "0.10" [libraries] # Project android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" } +# https://developer.android.com/studio/write/java8-support#library-desugaring-versions +android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3" kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } # https://firebase.google.com/docs/android/setup#available-libraries google_firebase_bom = "com.google.firebase:firebase-bom:32.0.0" diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 73e09d2314..78212a1b9c 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -61,5 +61,5 @@ dependencies { implementation(projects.features.login.impl) implementation(projects.features.networkmonitor.impl) implementation(libs.coroutines.core) - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") + coreLibraryDesugaring(libs.android.desugar) } From 9be8112c9cc986c54f058305b6c711af9d87e2d3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 May 2023 17:32:55 +0200 Subject: [PATCH 30/34] Add test for TimelineItemGrouper and fix a bug: the last group if any was not added to the list. --- .../timeline/groups/TimelineItemGrouper.kt | 35 ++-- .../groups/TimelineItemGrouperTest.kt | 170 ++++++++++++++++++ 2 files changed, 194 insertions(+), 11 deletions(-) create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt index 622400ee93..3cd1b8fd4a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt @@ -46,22 +46,15 @@ class TimelineItemGrouper @Inject constructor() { // timelineItem cannot be grouped if (currentGroup.isNotEmpty()) { // There is a pending group, create a TimelineItem.GroupedEvents if there is more than 1 Event in the pending group. - if (currentGroup.size == 1) { - // Do not create a group with just 1 item, just add the item to the result - result.add(currentGroup.first()) - } else { - result.add( - TimelineItem.GroupedEvents( - expanded = expandedGroups[currentGroup.first().id + "_group"].orFalse(), - events = currentGroup.toImmutableList() - ) - ) - } + result.addGroup(currentGroup, expandedGroups) currentGroup.clear() } result.add(timelineItem) } } + if (currentGroup.isNotEmpty()) { + result.addGroup(currentGroup, expandedGroups) + } return result } @@ -80,3 +73,23 @@ class TimelineItemGrouper @Inject constructor() { } } } + +/** + * Will add a group if there is more than 1 item, else add the item to the list. + */ +private fun MutableList.addGroup( + group: MutableList, + expandedGroups: Map, +) { + if (group.size == 1) { + // Do not create a group with just 1 item, just add the item to the result + add(group.first()) + } else { + add( + TimelineItem.GroupedEvents( + expanded = expandedGroups[group.first().id + "_group"].orFalse(), + events = group.toImmutableList() + ) + ) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt new file mode 100644 index 0000000000..5829bbad9f --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt @@ -0,0 +1,170 @@ +/* + * 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.messages.timeline.groups + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.fixtures.aMessageEvent +import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper +import io.element.android.features.messages.impl.timeline.model.AggregatedReaction +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent +import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel +import io.element.android.libraries.designsystem.components.avatar.anAvatarData +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 +import io.element.android.libraries.matrix.test.A_USER_ID +import kotlinx.collections.immutable.toImmutableList +import org.junit.Test + +class TimelineItemGrouperTest { + private val sut = TimelineItemGrouper() + + private val aGroupableItem = TimelineItem.Event( + id = AN_EVENT_ID.value, + senderId = A_USER_ID, + senderAvatar = anAvatarData(), + senderDisplayName = "", + content = TimelineItemStateEventContent(body = "a state event"), + reactionsState = TimelineItemReactions(emptyList().toImmutableList()) + ) + private val aNonGroupableItem = aMessageEvent() + private val aNonGroupableItemNoEvent = TimelineItem.Virtual("virtual", aTimelineItemDaySeparatorModel("Today")) + + @Test + fun `test empty`() { + val result = sut.group(emptyList(), emptyMap()) + assertThat(result).isEmpty() + } + + @Test + fun `test non groupables`() { + val result = sut.group( + listOf( + aNonGroupableItem, + aNonGroupableItem, + ), + emptyMap() + ) + assertThat(result).isEqualTo( + listOf( + aNonGroupableItem, + aNonGroupableItem, + ) + ) + } + + @Test + fun `test groupables and ensure reordering`() { + val result = sut.group( + listOf( + aGroupableItem.copy(id = AN_EVENT_ID_2.value), + aGroupableItem, + ), + emptyMap() + ) + assertThat(result).isEqualTo( + listOf( + TimelineItem.GroupedEvents( + expanded = false, + events = listOf( + aGroupableItem, + aGroupableItem.copy(id = AN_EVENT_ID_2.value), + ).toImmutableList() + ), + ) + ) + } + + @Test + fun `test groupables expanded`() { + val result = sut.group( + listOf( + aGroupableItem, + aGroupableItem.copy(id = AN_EVENT_ID_2.value), + ), + mapOf("${AN_EVENT_ID_2.value}_group" to true) + ) + assertThat(result).isEqualTo( + listOf( + TimelineItem.GroupedEvents( + expanded = true, + events = listOf( + aGroupableItem.copy(id = AN_EVENT_ID_2.value), + aGroupableItem, + ).toImmutableList() + ), + ) + ) + } + + @Test + fun `test 1 groupable, not group must be created`() { + val listsToTest = listOf( + listOf(aGroupableItem), + listOf(aGroupableItem, aNonGroupableItem), + listOf(aGroupableItem, aNonGroupableItemNoEvent), + listOf(aNonGroupableItem, aGroupableItem), + listOf(aNonGroupableItemNoEvent, aGroupableItem), + listOf(aNonGroupableItem, aGroupableItem, aNonGroupableItem), + listOf(aNonGroupableItemNoEvent, aGroupableItem, aNonGroupableItemNoEvent), + listOf(aGroupableItem, aNonGroupableItem, aGroupableItem), + listOf(aGroupableItem, aNonGroupableItemNoEvent, aGroupableItem), + listOf(aNonGroupableItem), + listOf(aNonGroupableItemNoEvent), + ) + listsToTest.forEach { listToTest -> + val result = sut.group(listToTest, emptyMap()) + assertThat(result).isEqualTo(listToTest) + } + } + + @Test + fun `test 3 blocks`() { + val result = sut.group( + listOf( + aGroupableItem, + aGroupableItem, + aNonGroupableItem, + aGroupableItem, + aGroupableItem, + aGroupableItem, + ), + emptyMap() + ) + assertThat(result).isEqualTo( + listOf( + TimelineItem.GroupedEvents( + expanded = false, + events = listOf( + aGroupableItem, + aGroupableItem, + ).toImmutableList() + ), + aNonGroupableItem, + TimelineItem.GroupedEvents( + expanded = false, + events = listOf( + aGroupableItem, + aGroupableItem, + aGroupableItem, + ).toImmutableList() + ) + ) + ) + } +} From 6102ffa7e4b202b1f2d032303982c0adb852d75f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 May 2023 18:10:32 +0200 Subject: [PATCH 31/34] Add test for TimelinePresenter: Collapse and expand group. --- .../timeline/TimelinePresenterTest.kt | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index e8fe681692..7b13a93a5a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -24,8 +24,12 @@ import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.anEventTimelineItem +import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import kotlinx.coroutines.test.runTest import org.junit.Test @@ -91,4 +95,37 @@ class TimelinePresenterTest { assertThat(withoutHighlightedState.highlightedEventId).isNull() } } + + @Test + fun `present - expand and collapse grouped events`() = runTest { + val fakeTimeline = FakeMatrixTimeline( + initialTimelineItems = listOf( + MatrixTimelineItem.Event(anEventTimelineItem() /* This is a groupable event */), + MatrixTimelineItem.Event(anEventTimelineItem() /* This is a groupable event */), + ) + ) + val fakeRoom = FakeMatrixRoom(matrixTimeline = fakeTimeline) + val presenter = TimelinePresenter( + timelineItemsFactory = aTimelineItemsFactory(), + timelineItemGrouper = TimelineItemGrouper(), + room = fakeRoom, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + fakeTimeline.updateTimelineItems { it } + val loadedState = awaitItem() + val group1 = loadedState.timelineItems.first() as TimelineItem.GroupedEvents + assertThat(group1.expanded).isFalse() + loadedState.eventSink.invoke(TimelineEvents.ToggleExpandGroup(group1)) + val withExpandedGroup = awaitItem() + val group2 = withExpandedGroup.timelineItems.first() as TimelineItem.GroupedEvents + assertThat(group2.expanded).isTrue() + withExpandedGroup.eventSink.invoke(TimelineEvents.ToggleExpandGroup(group2)) + val withCollapsedGroup = awaitItem() + val group3 = withCollapsedGroup.timelineItems.first() as TimelineItem.GroupedEvents + assertThat(group3.expanded).isFalse() + } + } } From b145fcbc50518234c279be1a080760dd95e63f71 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 26 May 2023 07:51:41 +0200 Subject: [PATCH 32/34] Enable one last ignored test for LeaveRoomPresenterImpl (#462) Most of the tests in `LeaveRoomPresenterImplTest.kt` where using `UnconfinedTestDispatcher` which was conflating some of the state returned by the presenter. This prevented to test one specific case which had been left with an `@Ignore` annotation. This PR switches to `StandardTestDispatcher` so that the ignored test case can work and also fixes some other test cases whose behavior is now more correct under the `StandardTestDispatcher`. Also updates our test factory method for `CoroutineDispatchers` to more easily obtain a `CoroutineDispatchers` which uses `StandardTestDispatcher`. --- .../impl/LeaveRoomPresenterImplTest.kt | 30 +++++-------- .../testutils/TestCoroutineDispatchers.kt | 42 +++++++++++-------- 2 files changed, 34 insertions(+), 38 deletions(-) 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 6dfc23be3f..5122f34f31 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 @@ -23,7 +23,6 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.leaveroom.api.LeaveRoomState -import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState @@ -35,11 +34,9 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest -import org.junit.Ignore import org.junit.Test class LeaveRoomPresenterImplTest { @@ -151,13 +148,10 @@ class LeaveRoomPresenterImplTest { }.test { val initialState = awaitItem() initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + // Membership observer should receive a 'left room' change + assertThat(roomMembershipObserver.updates.first().change).isEqualTo(MembershipChange.LEFT) cancelAndIgnoreRemainingEvents() } - - // Membership observer should receive a 'left room' change - roomMembershipObserver.updates.take(1) - .onEach { update -> assertThat(update.change).isEqualTo(MembershipChange.LEFT) } - .collect() } @Test @@ -177,15 +171,15 @@ class LeaveRoomPresenterImplTest { }.test { val initialState = awaitItem() initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + skipItems(1) // Skip show progress state val errorState = awaitItem() assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown) + cancelAndIgnoreRemainingEvents() } } @Test - @Ignore("TODO(Test the hiding/showing of the progress indicator too)") fun `present - show progress indicator while leaving a room`() = runTest { - val roomMembershipObserver = RoomMembershipObserver() val presenter = createPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( @@ -204,11 +198,6 @@ class LeaveRoomPresenterImplTest { val finalState = awaitItem() assertThat(finalState.progress).isEqualTo(LeaveRoomState.Progress.Hidden) } - - // Membership observer should receive a 'left room' change - roomMembershipObserver.updates.take(1) - .onEach { update -> assertThat(update.change).isEqualTo(MembershipChange.LEFT) } - .collect() } @Test @@ -228,8 +217,10 @@ class LeaveRoomPresenterImplTest { }.test { val initialState = awaitItem() initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID)) + skipItems(1) // Skip show progress state val errorState = awaitItem() assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown) + skipItems(1) // Skip hide progress state errorState.eventSink(LeaveRoomEvent.HideError) val hiddenErrorState = awaitItem() assertThat(hiddenErrorState.error).isEqualTo(LeaveRoomState.Error.Hidden) @@ -237,12 +228,11 @@ class LeaveRoomPresenterImplTest { } } -private fun createPresenter( +private fun TestScope.createPresenter( client: MatrixClient = FakeMatrixClient(), roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), - dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), ): LeaveRoomPresenter = LeaveRoomPresenterImpl( client = client, roomMembershipObserver = roomMembershipObserver, - dispatchers = dispatchers, + dispatchers = testCoroutineDispatchers(testScheduler, false), ) diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt index e155eabd73..a19dd629ae 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt @@ -17,27 +17,33 @@ package io.element.android.tests.testutils import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestCoroutineScheduler -import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher +/** + * Create a [CoroutineDispatchers] instance for testing. + * + * @param testScheduler The [TestCoroutineScheduler] to use. If using [runTest] use the one provided by its [TestScope]. + * If null the [TestDispatcher] logic will select one or create a new one. + * @param useUnconfinedTestDispatcher If true, use [UnconfinedTestDispatcher] for all dispatchers. + * If false, use [StandardTestDispatcher] for all dispatchers. + */ fun testCoroutineDispatchers( testScheduler: TestCoroutineScheduler? = null, -) = CoroutineDispatchers( - io = UnconfinedTestDispatcher(testScheduler), - computation = UnconfinedTestDispatcher(testScheduler), - main = UnconfinedTestDispatcher(testScheduler), - diffUpdateDispatcher = UnconfinedTestDispatcher(testScheduler), -) + useUnconfinedTestDispatcher: Boolean = true, +): CoroutineDispatchers = when (useUnconfinedTestDispatcher) { + false -> CoroutineDispatchers( + io = StandardTestDispatcher(testScheduler), + computation = StandardTestDispatcher(testScheduler), + main = StandardTestDispatcher(testScheduler), + diffUpdateDispatcher = StandardTestDispatcher(testScheduler), + ) -fun testCoroutineDispatchers( - io: TestDispatcher = UnconfinedTestDispatcher(), - computation: TestDispatcher = UnconfinedTestDispatcher(), - main: TestDispatcher = UnconfinedTestDispatcher(), - diffUpdateDispatcher: TestDispatcher = UnconfinedTestDispatcher(), -) = CoroutineDispatchers( - io = io, - computation = computation, - main = main, - diffUpdateDispatcher = diffUpdateDispatcher, -) + true -> CoroutineDispatchers( + io = UnconfinedTestDispatcher(testScheduler), + computation = UnconfinedTestDispatcher(testScheduler), + main = UnconfinedTestDispatcher(testScheduler), + diffUpdateDispatcher = UnconfinedTestDispatcher(testScheduler), + ) +} From 5f8eb7b9fdad50f97c811e38ca9a405e942fc987 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 May 2023 09:56:58 +0200 Subject: [PATCH 33/34] Update anvil to v2.4.6 (#463) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9f6b9c7d63..e70c367d20 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,7 +44,7 @@ sqldelight = "1.5.5" # DI dagger = "2.46.1" -anvil = "2.4.5" +anvil = "2.4.6" # quality detekt = "1.22.0" From efcd969b4c86fe2bb663ababe31a6cfa962673a2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 May 2023 11:04:18 +0200 Subject: [PATCH 34/34] Update mobile-dev-inc/action-maestro-cloud action to v1.3.3 (#457) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/maestro.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index 1fa53d24ef..f16a282d37 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -42,7 +42,7 @@ jobs: name: elementx-debug path: | app/build/outputs/apk/debug/*.apk - - uses: mobile-dev-inc/action-maestro-cloud@v1.3.2 + - uses: mobile-dev-inc/action-maestro-cloud@v1.3.3 with: api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} app-file: app/build/outputs/apk/debug/app-universal-debug.apk