diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt index 01c1164741..f7a0c279f9 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt @@ -12,8 +12,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import im.vector.app.features.analytics.plan.SuperProperties -import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter -import io.element.android.features.rageshake.api.detection.RageshakeDetectionPresenter +import io.element.android.features.rageshake.api.crash.CrashDetectionState +import io.element.android.features.rageshake.api.detection.RageshakeDetectionState import io.element.android.features.share.api.ShareService import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.SdkMetadata @@ -22,8 +22,8 @@ import io.element.android.services.apperror.api.AppErrorStateService import javax.inject.Inject class RootPresenter @Inject constructor( - private val crashDetectionPresenter: CrashDetectionPresenter, - private val rageshakeDetectionPresenter: RageshakeDetectionPresenter, + private val crashDetectionPresenter: Presenter, + private val rageshakeDetectionPresenter: Presenter, private val appErrorStateService: AppErrorStateService, private val analyticsService: AnalyticsService, private val shareService: ShareService, diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt index 619f56e51d..2312c2dbb0 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt @@ -12,17 +12,11 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.appnav.root.RootPresenter -import io.element.android.features.rageshake.impl.crash.DefaultCrashDetectionPresenter -import io.element.android.features.rageshake.impl.detection.DefaultRageshakeDetectionPresenter -import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter -import io.element.android.features.rageshake.test.crash.FakeCrashDataStore -import io.element.android.features.rageshake.test.rageshake.FakeRageShake -import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore -import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder +import io.element.android.features.rageshake.api.crash.aCrashDetectionState +import io.element.android.features.rageshake.api.detection.aRageshakeDetectionState import io.element.android.features.share.api.ShareService import io.element.android.features.share.test.FakeShareService import io.element.android.libraries.matrix.test.FakeSdkMetadata -import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.apperror.api.AppErrorState import io.element.android.services.apperror.api.AppErrorStateService @@ -44,7 +38,6 @@ class RootPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() assertThat(initialState.crashDetectionState.crashDetected).isFalse() } @@ -61,7 +54,7 @@ class RootPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(2) + skipItems(1) lambda.assertions().isCalledOnce() } } @@ -76,8 +69,6 @@ class RootPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) - val initialState = awaitItem() assertThat(initialState.errorState).isInstanceOf(AppErrorState.Error::class.java) val initialErrorState = initialState.errorState as AppErrorState.Error @@ -93,25 +84,9 @@ class RootPresenterTest { appErrorService: AppErrorStateService = DefaultAppErrorStateService(), shareService: ShareService = FakeShareService {}, ): RootPresenter { - val crashDataStore = FakeCrashDataStore() - val rageshakeDataStore = FakeRageshakeDataStore() - val rageshake = FakeRageShake() - val screenshotHolder = FakeScreenshotHolder() - val crashDetectionPresenter = DefaultCrashDetectionPresenter( - buildMeta = aBuildMeta(), - crashDataStore = crashDataStore - ) - val rageshakeDetectionPresenter = DefaultRageshakeDetectionPresenter( - screenshotHolder = screenshotHolder, - rageShake = rageshake, - preferencesPresenter = DefaultRageshakePreferencesPresenter( - rageshake = rageshake, - rageshakeDataStore = rageshakeDataStore, - ) - ) return RootPresenter( - crashDetectionPresenter = crashDetectionPresenter, - rageshakeDetectionPresenter = rageshakeDetectionPresenter, + crashDetectionPresenter = { aCrashDetectionState() }, + rageshakeDetectionPresenter = { aRageshakeDetectionState() }, appErrorStateService = appErrorService, analyticsService = FakeAnalyticsService(), shareService = shareService, diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesPresenter.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesPresenter.kt deleted file mode 100644 index 81108b2376..0000000000 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesPresenter.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.analytics.api.preferences - -import io.element.android.libraries.architecture.Presenter - -interface AnalyticsPreferencesPresenter : Presenter diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt new file mode 100644 index 0000000000..a8172896f8 --- /dev/null +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.analytics.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState +import io.element.android.features.analytics.impl.preferences.AnalyticsPreferencesPresenter +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.AppScope + +@ContributesTo(AppScope::class) +@Module +interface AnalyticsModule { + @Binds + fun bindAnalyticsPreferencesPresenter(presenter: AnalyticsPreferencesPresenter): Presenter +} diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/DefaultAnalyticsPreferencesPresenter.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt similarity index 83% rename from features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/DefaultAnalyticsPreferencesPresenter.kt rename to features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt index b715f73f6f..22cb8f603c 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/DefaultAnalyticsPreferencesPresenter.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt @@ -10,23 +10,20 @@ package io.element.android.features.analytics.impl.preferences import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.rememberCoroutineScope -import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.AnalyticsConfig import io.element.android.features.analytics.api.AnalyticsOptInEvents -import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.di.AppScope import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject -@ContributesBinding(AppScope::class) -class DefaultAnalyticsPreferencesPresenter @Inject constructor( +class AnalyticsPreferencesPresenter @Inject constructor( private val analyticsService: AnalyticsService, private val buildMeta: BuildMeta, -) : AnalyticsPreferencesPresenter { +) : Presenter { @Composable override fun present(): AnalyticsPreferencesState { val localCoroutineScope = rememberCoroutineScope() diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt index 14f6badfe8..1aa2bc26b4 100644 --- a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt +++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt @@ -25,7 +25,7 @@ class AnalyticsPreferencesPresenterTest { @Test fun `present - initial state available`() = runTest { - val presenter = DefaultAnalyticsPreferencesPresenter( + val presenter = AnalyticsPreferencesPresenter( FakeAnalyticsService(isEnabled = true, didAskUserConsent = true), aBuildMeta() ) @@ -41,7 +41,7 @@ class AnalyticsPreferencesPresenterTest { @Test fun `present - initial state not available`() = runTest { - val presenter = DefaultAnalyticsPreferencesPresenter( + val presenter = AnalyticsPreferencesPresenter( FakeAnalyticsService(isEnabled = false, didAskUserConsent = false), aBuildMeta() ) @@ -55,7 +55,7 @@ class AnalyticsPreferencesPresenterTest { @Test fun `present - enable and disable`() = runTest { - val presenter = DefaultAnalyticsPreferencesPresenter( + val presenter = AnalyticsPreferencesPresenter( FakeAnalyticsService(isEnabled = true, didAskUserConsent = true), aBuildMeta() ) diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt index 2abf5b8d18..10189c3c67 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt @@ -15,18 +15,19 @@ import io.element.android.features.ftue.impl.state.DefaultFtueService import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.test.FakeLockScreenService +import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.libraries.permissions.api.PermissionStateProvider import io.element.android.libraries.permissions.impl.FakePermissionStateProvider +import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -36,8 +37,9 @@ class DefaultFtueServiceTest { val sessionVerificationService = FakeSessionVerificationService().apply { givenVerifiedStatus(SessionVerifiedStatus.Unknown) } - val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) - val service = createDefaultFtueService(coroutineScope, sessionVerificationService) + val service = createDefaultFtueService( + sessionVerificationService = sessionVerificationService, + ) service.state.test { // Verification state is unknown, we don't display the flow yet @@ -47,9 +49,6 @@ class DefaultFtueServiceTest { sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified) assertThat(awaitItem()).isEqualTo(FtueState.Incomplete) } - - // Cleanup - coroutineScope.cancel() } @Test @@ -58,10 +57,7 @@ class DefaultFtueServiceTest { val sessionVerificationService = FakeSessionVerificationService() val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true) val lockScreenService = FakeLockScreenService() - val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) - val service = createDefaultFtueService( - coroutineScope = coroutineScope, sessionVerificationService = sessionVerificationService, analyticsService = analyticsService, permissionStateProvider = permissionStateProvider, @@ -75,9 +71,6 @@ class DefaultFtueServiceTest { service.updateState() assertThat(service.state.value).isEqualTo(FtueState.Complete) - - // Cleanup - coroutineScope.cancel() } @Test @@ -88,10 +81,7 @@ class DefaultFtueServiceTest { val analyticsService = FakeAnalyticsService() val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) val lockScreenService = FakeLockScreenService() - val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) - val service = createDefaultFtueService( - coroutineScope = coroutineScope, sessionVerificationService = sessionVerificationService, analyticsService = analyticsService, permissionStateProvider = permissionStateProvider, @@ -126,20 +116,15 @@ class DefaultFtueServiceTest { // Final state null, ) - - // Cleanup - coroutineScope.cancel() } @Test fun `if a check for a step is true, start from the next one`() = runTest { - val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val sessionVerificationService = FakeSessionVerificationService() val analyticsService = FakeAnalyticsService() val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) val lockScreenService = FakeLockScreenService() val service = createDefaultFtueService( - coroutineScope = coroutineScope, sessionVerificationService = sessionVerificationService, analyticsService = analyticsService, permissionStateProvider = permissionStateProvider, @@ -155,14 +140,10 @@ class DefaultFtueServiceTest { analyticsService.setDidAskUserConsent() assertThat(service.getNextStep(null)).isNull() - - // Cleanup - coroutineScope.cancel() } @Test fun `if version is older than 13 we don't display the notification opt in screen`() = runTest { - val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val sessionVerificationService = FakeSessionVerificationService() val analyticsService = FakeAnalyticsService() val lockScreenService = FakeLockScreenService() @@ -170,7 +151,6 @@ class DefaultFtueServiceTest { val service = createDefaultFtueService( sdkIntVersion = Build.VERSION_CODES.M, sessionVerificationService = sessionVerificationService, - coroutineScope = coroutineScope, analyticsService = analyticsService, lockScreenService = lockScreenService, ) @@ -182,14 +162,10 @@ class DefaultFtueServiceTest { analyticsService.setDidAskUserConsent() assertThat(service.getNextStep(null)).isNull() - - // Cleanup - coroutineScope.cancel() } @Test fun `reset do the expected actions S`() = runTest { - val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val resetAnalyticsLambda = lambdaRecorder { } val analyticsService = FakeAnalyticsService( resetLambda = resetAnalyticsLambda @@ -199,7 +175,6 @@ class DefaultFtueServiceTest { resetPermissionLambda = resetPermissionLambda ) val service = createDefaultFtueService( - coroutineScope = coroutineScope, sdkIntVersion = Build.VERSION_CODES.S, permissionStateProvider = permissionStateProvider, analyticsService = analyticsService, @@ -211,7 +186,6 @@ class DefaultFtueServiceTest { @Test fun `reset do the expected actions TIRAMISU`() = runTest { - val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val resetLambda = lambdaRecorder { } val analyticsService = FakeAnalyticsService( resetLambda = resetLambda @@ -221,7 +195,6 @@ class DefaultFtueServiceTest { resetPermissionLambda = resetPermissionLambda ) val service = createDefaultFtueService( - coroutineScope = coroutineScope, sdkIntVersion = Build.VERSION_CODES.TIRAMISU, permissionStateProvider = permissionStateProvider, analyticsService = analyticsService, @@ -232,17 +205,16 @@ class DefaultFtueServiceTest { .with(value("android.permission.POST_NOTIFICATIONS")) } - private fun createDefaultFtueService( - coroutineScope: CoroutineScope, - sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), + private fun TestScope.createDefaultFtueService( + sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(), analyticsService: AnalyticsService = FakeAnalyticsService(), - permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false), + permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(permissionGranted = false), lockScreenService: LockScreenService = FakeLockScreenService(), - sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), + sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), // First version where notification permission is required sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU, ) = DefaultFtueService( - sessionCoroutineScope = coroutineScope, + sessionCoroutineScope = backgroundScope, sessionVerificationService = sessionVerificationService, sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion), analyticsService = analyticsService, 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 deleted file mode 100644 index f7d7cf7297..0000000000 --- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomPresenter.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -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/LeaveRoomStateProvider.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt index eb7a90fd31..fef638310c 100644 --- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomStateProvider.kt @@ -57,9 +57,10 @@ fun aLeaveRoomState( confirmation: LeaveRoomState.Confirmation = LeaveRoomState.Confirmation.Hidden, progress: LeaveRoomState.Progress = LeaveRoomState.Progress.Hidden, error: LeaveRoomState.Error = LeaveRoomState.Error.Hidden, + eventSink: (LeaveRoomEvent) -> Unit = {}, ) = LeaveRoomState( confirmation = confirmation, progress = progress, error = error, - eventSink = {}, + eventSink = eventSink, ) diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/DefaultLeaveRoomPresenter.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt similarity index 93% rename from features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/DefaultLeaveRoomPresenter.kt rename to features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt index bb249846ee..168a684922 100644 --- a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/DefaultLeaveRoomPresenter.kt +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt @@ -12,16 +12,14 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import com.squareup.anvil.annotations.ContributesBinding 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.Dm 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.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -30,12 +28,11 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -@ContributesBinding(SessionScope::class) -class DefaultLeaveRoomPresenter @Inject constructor( +class LeaveRoomPresenter @Inject constructor( private val client: MatrixClient, private val roomMembershipObserver: RoomMembershipObserver, private val dispatchers: CoroutineDispatchers, -) : LeaveRoomPresenter { +) : Presenter { @Composable override fun present(): LeaveRoomState { val scope = rememberCoroutineScope() diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt new file mode 100644 index 0000000000..8dbe4db368 --- /dev/null +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.leaveroom.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.features.leaveroom.impl.LeaveRoomPresenter +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope + +@ContributesTo(SessionScope::class) +@Module +interface LeaveRoomModule { + @Binds + fun bindLeaveRoomPresenter(presenter: LeaveRoomPresenter): Presenter +} diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/DefaultLeaveRoomPresenterTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterTest.kt similarity index 92% rename from features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/DefaultLeaveRoomPresenterTest.kt rename to features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterTest.kt index 1454c84296..9689de4eb2 100644 --- a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/DefaultLeaveRoomPresenterTest.kt +++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterTest.kt @@ -27,13 +27,13 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -class DefaultLeaveRoomPresenterTest { +class LeaveRoomPresenterTest { @get:Rule val warmUpRule = WarmUpRule() @Test fun `present - initial state hides all dialogs`() = runTest { - val presenter = createDefaultLeaveRoomPresenter() + val presenter = createLeaveRoomPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -46,7 +46,7 @@ class DefaultLeaveRoomPresenterTest { @Test fun `present - show generic confirmation`() = runTest { - val presenter = createDefaultLeaveRoomPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -66,7 +66,7 @@ class DefaultLeaveRoomPresenterTest { @Test fun `present - show private room confirmation`() = runTest { - val presenter = createDefaultLeaveRoomPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -86,7 +86,7 @@ class DefaultLeaveRoomPresenterTest { @Test fun `present - show last user in room confirmation`() = runTest { - val presenter = createDefaultLeaveRoomPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -106,7 +106,7 @@ class DefaultLeaveRoomPresenterTest { @Test fun `present - show DM confirmation`() = runTest { - val presenter = createDefaultLeaveRoomPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -127,7 +127,7 @@ class DefaultLeaveRoomPresenterTest { @Test fun `present - leaving a room leaves the room`() = runTest { val roomMembershipObserver = RoomMembershipObserver() - val presenter = createDefaultLeaveRoomPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -151,7 +151,7 @@ class DefaultLeaveRoomPresenterTest { @Test fun `present - show error if leave room fails`() = runTest { - val presenter = createDefaultLeaveRoomPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -175,7 +175,7 @@ class DefaultLeaveRoomPresenterTest { @Test fun `present - show progress indicator while leaving a room`() = runTest { - val presenter = createDefaultLeaveRoomPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -199,7 +199,7 @@ class DefaultLeaveRoomPresenterTest { @Test fun `present - hide error hides the error`() = runTest { - val presenter = createDefaultLeaveRoomPresenter( + val presenter = createLeaveRoomPresenter( client = FakeMatrixClient().apply { givenGetRoomResult( roomId = A_ROOM_ID, @@ -225,10 +225,10 @@ class DefaultLeaveRoomPresenterTest { } } -private fun TestScope.createDefaultLeaveRoomPresenter( +private fun TestScope.createLeaveRoomPresenter( client: MatrixClient = FakeMatrixClient(), roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), -): DefaultLeaveRoomPresenter = DefaultLeaveRoomPresenter( +): LeaveRoomPresenter = LeaveRoomPresenter( client = client, roomMembershipObserver = roomMembershipObserver, dispatchers = testCoroutineDispatchers(false), diff --git a/features/leaveroom/test/build.gradle.kts b/features/leaveroom/test/build.gradle.kts deleted file mode 100644 index 395542cab1..0000000000 --- a/features/leaveroom/test/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2022-2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -plugins { - id("io.element.android-compose-library") -} - -android { - namespace = "io.element.android.features.leaveroom.test" -} - -dependencies { - implementation(projects.libraries.core) - implementation(projects.libraries.architecture) - implementation(projects.libraries.matrix.api) - api(projects.features.leaveroom.api) -} diff --git a/features/leaveroom/test/src/main/kotlin/io/element/android/features/leaveroom/fake/FakeLeaveRoomPresenter.kt b/features/leaveroom/test/src/main/kotlin/io/element/android/features/leaveroom/fake/FakeLeaveRoomPresenter.kt deleted file mode 100644 index 2aa9961c39..0000000000 --- a/features/leaveroom/test/src/main/kotlin/io/element/android/features/leaveroom/fake/FakeLeaveRoomPresenter.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -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 - -class FakeLeaveRoomPresenter : LeaveRoomPresenter { - val events = mutableListOf() - - private fun handleEvent(event: LeaveRoomEvent) { - events += event - } - - private var state = LeaveRoomState( - confirmation = LeaveRoomState.Confirmation.Hidden, - progress = LeaveRoomState.Progress.Hidden, - error = LeaveRoomState.Error.Hidden, - eventSink = ::handleEvent, - ) - set(value) { - field = value.copy(eventSink = ::handleEvent) - } - - fun givenState(state: LeaveRoomState) { - this.state = state - } - - @Composable - override fun present(): LeaveRoomState = state -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt new file mode 100644 index 0000000000..02b32ad66d --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.login.impl.changeserver.ChangeServerPresenter +import io.element.android.features.login.impl.changeserver.ChangeServerState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.AppScope + +@ContributesTo(AppScope::class) +@Module +interface LoginModule { + @Binds + fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt index 83f0a4f1b3..de68fdd3f4 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -10,12 +10,12 @@ package io.element.android.features.login.impl.screens.changeaccountprovider import androidx.compose.runtime.Composable import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.changeserver.ChangeServerPresenter +import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.libraries.architecture.Presenter import javax.inject.Inject class ChangeAccountProviderPresenter @Inject constructor( - private val changeServerPresenter: ChangeServerPresenter, + private val changeServerPresenter: Presenter, ) : Presenter { @Composable override fun present(): ChangeAccountProviderState { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt index 06ae31d6ca..710f18a4f5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt @@ -15,7 +15,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import io.element.android.features.login.impl.changeserver.ChangeServerPresenter +import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.resolver.HomeserverData import io.element.android.features.login.impl.resolver.HomeserverResolver import io.element.android.libraries.architecture.AsyncData @@ -27,7 +27,7 @@ import javax.inject.Inject class SearchAccountProviderPresenter @Inject constructor( private val homeserverResolver: HomeserverResolver, - private val changeServerPresenter: ChangeServerPresenter, + private val changeServerPresenter: Presenter, ) : Presenter { @Composable override fun present(): SearchAccountProviderState { diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt index bd653bbfae..6f71edf6f1 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt @@ -12,9 +12,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.changeserver.ChangeServerPresenter -import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -26,12 +24,8 @@ class ChangeAccountProviderPresenterTest { @Test fun `present - initial state`() = runTest { - val changeServerPresenter = ChangeServerPresenter( - FakeMatrixAuthenticationService(), - AccountProviderDataSource() - ) val presenter = ChangeAccountProviderPresenter( - changeServerPresenter + changeServerPresenter = { aChangeServerState() } ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt index a7529643ba..91b2698d97 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt @@ -11,15 +11,13 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.changeserver.ChangeServerPresenter +import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.resolver.HomeserverResolver import io.element.android.features.login.impl.resolver.network.FakeWellknownRequest import io.element.android.features.login.impl.resolver.network.WellKnown import io.element.android.features.login.impl.resolver.network.WellKnownBaseConfig import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.test.A_HOMESERVER_URL -import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.runTest @@ -33,13 +31,9 @@ class SearchAccountProviderPresenterTest { @Test fun `present - initial state`() = runTest { val fakeWellknownRequest = FakeWellknownRequest() - val changeServerPresenter = ChangeServerPresenter( - FakeMatrixAuthenticationService(), - AccountProviderDataSource() - ) val presenter = SearchAccountProviderPresenter( - HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), - changeServerPresenter + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + changeServerPresenter = { aChangeServerState() } ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -53,13 +47,9 @@ class SearchAccountProviderPresenterTest { @Test fun `present - enter text no result`() = runTest { val fakeWellknownRequest = FakeWellknownRequest() - val changeServerPresenter = ChangeServerPresenter( - FakeMatrixAuthenticationService(), - AccountProviderDataSource() - ) val presenter = SearchAccountProviderPresenter( - HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), - changeServerPresenter + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + changeServerPresenter = { aChangeServerState() } ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -77,13 +67,9 @@ class SearchAccountProviderPresenterTest { @Test fun `present - enter valid url no wellknown`() = runTest { val fakeWellknownRequest = FakeWellknownRequest() - val changeServerPresenter = ChangeServerPresenter( - FakeMatrixAuthenticationService(), - AccountProviderDataSource() - ) val presenter = SearchAccountProviderPresenter( - HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), - changeServerPresenter + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + changeServerPresenter = { aChangeServerState() } ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -112,13 +98,9 @@ class SearchAccountProviderPresenterTest { "https://test.io" to aWellKnown(), ) ) - val changeServerPresenter = ChangeServerPresenter( - FakeMatrixAuthenticationService(), - AccountProviderDataSource() - ) val presenter = SearchAccountProviderPresenter( - HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), - changeServerPresenter + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + changeServerPresenter = { aChangeServerState() } ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutPresenter.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutPresenter.kt deleted file mode 100644 index c20e04b4f0..0000000000 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutPresenter.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.logout.api.direct - -import io.element.android.libraries.architecture.Presenter - -interface DirectLogoutPresenter : Presenter diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/di/LogoutModule.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/di/LogoutModule.kt new file mode 100644 index 0000000000..fdd157fef0 --- /dev/null +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/di/LogoutModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.logout.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.features.logout.impl.direct.DirectLogoutPresenter +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope + +@ContributesTo(SessionScope::class) +@Module +interface LogoutModule { + @Binds + fun bindDirectLogoutPresenter(presenter: DirectLogoutPresenter): Presenter +} diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenter.kt similarity index 90% rename from features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt rename to features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenter.kt index 1595747a31..4a5fead974 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenter.kt @@ -14,14 +14,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.logout.api.direct.DirectLogoutEvents -import io.element.android.features.logout.api.direct.DirectLogoutPresenter import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.impl.tools.isBackingUp import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState -import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EncryptionService @@ -29,11 +27,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject -@ContributesBinding(SessionScope::class) -class DefaultDirectLogoutPresenter @Inject constructor( +class DirectLogoutPresenter @Inject constructor( private val matrixClient: MatrixClient, private val encryptionService: EncryptionService, -) : DirectLogoutPresenter { +) : Presenter { @Composable override fun present(): DirectLogoutState { val localCoroutineScope = rememberCoroutineScope() diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt similarity index 92% rename from features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt rename to features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt index e8bcece709..a490b7b10d 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt @@ -26,13 +26,13 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -class DefaultDirectLogoutPresenterTest { +class DirectLogoutPresenterTest { @get:Rule val warmUpRule = WarmUpRule() @Test fun `present - initial state`() = runTest { - val presenter = createDefaultDirectLogoutPresenter() + val presenter = createDirectLogoutPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -44,7 +44,7 @@ class DefaultDirectLogoutPresenterTest { @Test fun `present - initial state - last session`() = runTest { - val presenter = createDefaultDirectLogoutPresenter( + val presenter = createDirectLogoutPresenter( encryptionService = FakeEncryptionService().apply { emitIsLastDevice(true) } @@ -66,7 +66,7 @@ class DefaultDirectLogoutPresenterTest { emit(BackupUploadState.Waiting) } ) - val presenter = createDefaultDirectLogoutPresenter( + val presenter = createDirectLogoutPresenter( encryptionService = encryptionService ) moleculeFlow(RecompositionMode.Immediate) { @@ -81,7 +81,7 @@ class DefaultDirectLogoutPresenterTest { @Test fun `present - logout then cancel`() = runTest { - val presenter = createDefaultDirectLogoutPresenter() + val presenter = createDirectLogoutPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -97,7 +97,7 @@ class DefaultDirectLogoutPresenterTest { @Test fun `present - logout then confirm`() = runTest { - val presenter = createDefaultDirectLogoutPresenter() + val presenter = createDirectLogoutPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -120,7 +120,7 @@ class DefaultDirectLogoutPresenterTest { throw A_THROWABLE } } - val presenter = createDefaultDirectLogoutPresenter( + val presenter = createDirectLogoutPresenter( matrixClient, ) moleculeFlow(RecompositionMode.Immediate) { @@ -152,7 +152,7 @@ class DefaultDirectLogoutPresenterTest { } } } - val presenter = createDefaultDirectLogoutPresenter( + val presenter = createDirectLogoutPresenter( matrixClient, ) moleculeFlow(RecompositionMode.Immediate) { @@ -179,10 +179,10 @@ class DefaultDirectLogoutPresenterTest { return awaitItem() } - private fun createDefaultDirectLogoutPresenter( + private fun createDirectLogoutPresenter( matrixClient: MatrixClient = FakeMatrixClient(), encryptionService: EncryptionService = FakeEncryptionService(), - ): DefaultDirectLogoutPresenter = DefaultDirectLogoutPresenter( + ): DirectLogoutPresenter = DirectLogoutPresenter( matrixClient = matrixClient, encryptionService = encryptionService, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 388780d384..236ea211fe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -32,22 +32,21 @@ 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.actionlist.model.TimelineItemActionPostProcessor import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents -import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState import io.element.android.features.messages.impl.timeline.TimelineController 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.TimelineState -import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter -import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter -import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState +import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState +import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent 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.protection.TimelineProtectionState -import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter +import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.libraries.androidutils.clipboard.ClipboardHelper @@ -88,14 +87,14 @@ import timber.log.Timber class MessagesPresenter @AssistedInject constructor( @Assisted private val navigator: MessagesNavigator, private val room: MatrixRoom, - private val composerPresenter: MessageComposerPresenter, - private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, + private val composerPresenter: Presenter, + private val voiceMessageComposerPresenter: Presenter, timelinePresenterFactory: TimelinePresenter.Factory, private val timelineProtectionPresenter: Presenter, private val actionListPresenterFactory: ActionListPresenter.Factory, - private val customReactionPresenter: CustomReactionPresenter, - private val reactionSummaryPresenter: ReactionSummaryPresenter, - private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter, + private val customReactionPresenter: Presenter, + private val reactionSummaryPresenter: Presenter, + private val readReceiptBottomSheetPresenter: Presenter, private val pinnedMessagesBannerPresenter: Presenter, private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt index 1a5aa1c7d1..31c1d6a758 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt @@ -12,12 +12,22 @@ import dagger.Binds import dagger.Module import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState +import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter +import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState +import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter +import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState +import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter +import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionPresenter import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.typing.TypingNotificationPresenter import io.element.android.features.messages.impl.typing.TypingNotificationState +import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter +import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope @@ -35,4 +45,19 @@ interface MessagesModule { @Binds fun bindTimelineProtectionPresenter(presenter: TimelineProtectionPresenter): Presenter + + @Binds + fun bindMessageComposerPresenter(presenter: MessageComposerPresenter): Presenter + + @Binds + fun bindVoiceMessageComposerPresenter(presenter: VoiceMessageComposerPresenter): Presenter + + @Binds + fun bindCustomReactionPresenter(presenter: CustomReactionPresenter): Presenter + + @Binds + fun bindReactionSummaryPresenter(presenter: ReactionSummaryPresenter): Presenter + + @Binds + fun bindReadReceiptBottomSheetPresenter(presenter: ReadReceiptBottomSheetPresenter): Presenter } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index 63a5eeb8c2..2730872072 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -33,6 +33,7 @@ fun aMessageComposerState( canCreatePoll: Boolean = true, attachmentsState: AttachmentsState = AttachmentsState.None, suggestions: ImmutableList = persistentListOf(), + eventSink: (MessageComposerEvents) -> Unit = {}, ) = MessageComposerState( textEditorState = textEditorState, isFullScreen = isFullScreen, @@ -44,5 +45,5 @@ fun aMessageComposerState( attachmentsState = attachmentsState, suggestions = suggestions, resolveMentionDisplay = { _, _ -> TextDisplay.Plain }, - eventSink = {}, + eventSink = eventSink, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 209d719b82..284f141d41 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -7,33 +7,23 @@ package io.element.android.features.messages.impl -import android.net.Uri import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow -import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.PinUnpinAction +import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction -import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aResolveVerifiedUserSendFailureState -import io.element.android.features.messages.impl.draft.FakeComposerDraftService import io.element.android.features.messages.impl.fixtures.aMessageEvent -import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator -import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext -import io.element.android.features.messages.impl.messagecomposer.FakeRoomAliasSuggestionsDataSource -import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter -import io.element.android.features.messages.impl.messagecomposer.TestRichTextEditorStateFactory -import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor +import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents +import io.element.android.features.messages.impl.messagecomposer.MessageComposerState +import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState import io.element.android.features.messages.impl.timeline.TimelineController -import io.element.android.features.messages.impl.timeline.TimelineItemIndexer import io.element.android.features.messages.impl.timeline.TimelinePresenter -import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter -import io.element.android.features.messages.impl.timeline.components.customreaction.FakeEmojibaseProvider -import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter -import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter +import io.element.android.features.messages.impl.timeline.createTimelinePresenter import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent @@ -41,25 +31,19 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState -import io.element.android.features.messages.impl.typing.aTypingNotificationState -import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper -import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer -import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter -import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager -import io.element.android.features.messages.test.FakeMessageComposerContext +import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.test.actions.FakeEndPollAction -import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId @@ -72,33 +56,24 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.timeline.FakeTimeline -import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails -import io.element.android.libraries.mediapickers.test.FakePickerProvider -import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer -import io.element.android.libraries.mediaupload.api.MediaSender -import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor -import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory -import io.element.android.libraries.permissions.api.PermissionsPresenter -import io.element.android.libraries.permissions.test.FakePermissionsPresenter -import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory -import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore -import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider +import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState import io.element.android.libraries.textcomposer.model.MessageComposerMode -import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder +import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilTimeout @@ -107,7 +82,6 @@ import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.testCoroutineDispatchers -import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -123,8 +97,6 @@ class MessagesPresenterTest { @get:Rule val warmUpRule = WarmUpRule() - private val mockMediaUrl: Uri = mockk("localMediaUri") - @Test fun `present - initial state`() = runTest { val presenter = createMessagesPresenter() @@ -212,15 +184,13 @@ class MessagesPresenterTest { }.test { skipItems(1) val initialState = awaitItem() - initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) - // No crashes when sending a reaction failed - timeline.apply { toggleReactionLambda = toggleReactionFailure } - initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) - assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) - + initialState.eventSink(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) assert(toggleReactionSuccess) .isCalledOnce() .with(value("👍"), value(A_UNIQUE_ID)) + // No crashes when sending a reaction failed + timeline.apply { toggleReactionLambda = toggleReactionFailure } + initialState.eventSink(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) assert(toggleReactionFailure) .isCalledOnce() .with(value("👍"), value(A_UNIQUE_ID)) @@ -248,15 +218,16 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) - initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) + initialState.eventSink(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) assert(toggleReactionSuccess) .isCalledExactly(2) .withSequence( listOf(value("👍"), value(A_UNIQUE_ID)), listOf(value("👍"), value(A_UNIQUE_ID)), ) + skipItems(1) } } @@ -267,9 +238,8 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent())) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(navigator.onForwardEventClickedCount).isEqualTo(1) } @@ -283,9 +253,9 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Copy, event)) - assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Copy, event)) + skipItems(2) assertThat(clipboardHelper.clipboardContents).isEqualTo((event.content as TimelineItemTextContent).body) } } @@ -310,24 +280,33 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.CopyLink, event)) - assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.CopyLink, event)) + skipItems(2) assertThat(clipboardHelper.clipboardContents).isEqualTo("a link") } } @Test fun `present - handle action reply`() = runTest { - val presenter = createMessagesPresenter() + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent())) - val finalState = awaitItem() - assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) - assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent())) + awaitItem() + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.Reply( + replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID), + hideImage = false, + ) + ) + ) } } @@ -337,21 +316,22 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) - assertThat(initialState.actionListState.target).isEqualTo(ActionListState.Target.None) - // Otherwise we would have some extra items here - ensureAllEventsConsumed() + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) + skipItems(1) } } @Test fun `present - handle action reply to an image media message`() = runTest { - val presenter = createMessagesPresenter() + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() val mediaMessage = aMessageEvent( content = TimelineItemImageContent( body = "image.jpg", @@ -368,22 +348,29 @@ class MessagesPresenterTest { formattedFileSize = "4MB" ) ) - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) - val finalState = awaitItem() - assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) - val replyMode = finalState.composerState.mode as MessageComposerMode.Reply - assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) - assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) + awaitItem() + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.Reply( + replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID), + hideImage = false, + ) + ) + ) } } @Test fun `present - handle action reply to a video media message`() = runTest { - val presenter = createMessagesPresenter() + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() val mediaMessage = aMessageEvent( content = TimelineItemVideoContent( body = "video.mp4", @@ -401,22 +388,29 @@ class MessagesPresenterTest { formattedFileSize = "50MB" ) ) - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) - val finalState = awaitItem() - assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) - val replyMode = finalState.composerState.mode as MessageComposerMode.Reply - assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) - assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) + awaitItem() + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.Reply( + replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID), + hideImage = false, + ) + ) + ) } } @Test fun `present - handle action reply to a file media message`() = runTest { - val presenter = createMessagesPresenter() + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() val mediaMessage = aMessageEvent( content = TimelineItemFileContent( body = "file.pdf", @@ -427,26 +421,40 @@ class MessagesPresenterTest { fileExtension = "pdf", ) ) - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) - val finalState = awaitItem() - assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) - val replyMode = finalState.composerState.mode as MessageComposerMode.Reply - assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) - assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) + awaitItem() + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.Reply( + replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID), + hideImage = false, + ) + ) + ) } } @Test fun `present - handle action edit`() = runTest { - val presenter = createMessagesPresenter() + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent())) - val finalState = awaitItem() - assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Edit::class.java) - assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent())) + awaitItem() + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.Edit( + eventId = AN_EVENT_ID, + transactionId = null, + content = (aMessageEvent().content as TimelineItemTextContent).body + ) + ) + ) } } @@ -457,8 +465,9 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent()))) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent()))) + awaitItem() assertThat(navigator.onEditPollClickedCount).isEqualTo(1) } } @@ -470,9 +479,9 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() endPollAction.verifyExecutionCount(0) - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent()))) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent()))) delay(1) endPollAction.verifyExecutionCount(1) cancelAndIgnoreRemainingEvents() @@ -496,16 +505,17 @@ class MessagesPresenterTest { val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(Unit) } liveTimeline.redactEventLambda = redactEventLambda - - val presenter = createMessagesPresenter(matrixRoom = matrixRoom, coroutineDispatchers = coroutineDispatchers) + val presenter = createMessagesPresenter( + matrixRoom = matrixRoom, + coroutineDispatchers = coroutineDispatchers, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() val messageEvent = aMessageEvent() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, messageEvent)) - assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Redact, messageEvent)) + awaitItem() assert(redactEventLambda) .isCalledOnce() .with(value(messageEvent.eventId), value(messageEvent.transactionId), value(null)) @@ -519,9 +529,8 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent())) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(navigator.onReportContentClickedCount).isEqualTo(1) } @@ -533,9 +542,8 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() - initialState.eventSink.invoke(MessagesEvents.Dismiss) + initialState.eventSink(MessagesEvents.Dismiss) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -547,9 +555,8 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ViewSource, aMessageEvent())) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ViewSource, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(navigator.onShowEventDebugInfoClickedCount).isEqualTo(1) } @@ -572,21 +579,18 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() // Initially the composer doesn't have focus, so we don't show the alert assertThat(initialState.showReinvitePrompt).isFalse() // When the input field is focused we show the alert - initialState.composerState.textEditorState.requestFocus() - val focusedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state -> - state.showReinvitePrompt - }.last() + (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true + skipItems(1) + val focusedState = awaitItem() assertThat(focusedState.showReinvitePrompt).isTrue() // If it's dismissed then we stop showing the alert initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel)) - val dismissedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state -> - !state.showReinvitePrompt - }.last() + skipItems(1) + val dismissedState = awaitItem() assertThat(dismissedState.showReinvitePrompt).isFalse() } } @@ -608,9 +612,9 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() assertThat(initialState.showReinvitePrompt).isFalse() - initialState.composerState.textEditorState.requestFocus() + (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true val focusedState = awaitItem() assertThat(focusedState.showReinvitePrompt).isFalse() } @@ -633,9 +637,9 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() assertThat(initialState.showReinvitePrompt).isFalse() - initialState.composerState.textEditorState.requestFocus() + (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true val focusedState = awaitItem() assertThat(focusedState.showReinvitePrompt).isFalse() } @@ -799,7 +803,8 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val state = awaitFirstItem() + skipItems(1) + val state = awaitItem() assertThat(state.userEventPermissions.canSendMessage).isTrue() } } @@ -826,9 +831,7 @@ class MessagesPresenterTest { }.test { // Default value assertThat(awaitItem().userEventPermissions.canSendMessage).isTrue() - skipItems(1) assertThat(awaitItem().userEventPermissions.canSendMessage).isFalse() - cancelAndIgnoreRemainingEvents() } } @@ -876,21 +879,27 @@ class MessagesPresenterTest { @Test fun `present - handle action reply to a poll`() = runTest { - val presenter = createMessagesPresenter() + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() val poll = aMessageEvent( content = aTimelineItemPollContent() ) - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, poll)) - val finalState = awaitItem() - assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) - val replyMode = finalState.composerState.mode as MessageComposerMode.Reply - - assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) - assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, poll)) + skipItems(1) + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.Reply( + replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID), + hideImage = false, + ) + ) + ) } } @@ -916,15 +925,16 @@ class MessagesPresenterTest { val messageEvent = aMessageEvent( content = aTimelineItemTextContent() ) - val initialState = awaitFirstItem() + val initialState = awaitItem() timeline.pinEventLambda = successPinEventLambda - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent)) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent)) assert(successPinEventLambda).isCalledOnce().with(value(messageEvent.eventId)) timeline.pinEventLambda = failurePinEventLambda - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent)) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent)) assert(failurePinEventLambda).isCalledOnce().with(value(messageEvent.eventId)) + skipItems(1) assertThat(awaitItem().snackbarMessage).isNotNull() assertThat(analyticsService.capturedEvents).containsExactly( PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline), @@ -955,15 +965,16 @@ class MessagesPresenterTest { val messageEvent = aMessageEvent( content = aTimelineItemTextContent() ) - val initialState = awaitFirstItem() + val initialState = awaitItem() timeline.unpinEventLambda = successUnpinEventLambda - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent)) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent)) assert(successUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId)) timeline.unpinEventLambda = failureUnpinEventLambda - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent)) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent)) assert(failureUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId)) + skipItems(1) assertThat(awaitItem().snackbarMessage).isNotNull() assertThat(analyticsService.capturedEvents).containsExactly( PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline), @@ -972,12 +983,6 @@ class MessagesPresenterTest { } } - private suspend fun ReceiveTurbine.awaitFirstItem(): T { - // Skip 2 item if Mentions feature is enabled, else 1 - skipItems(if (FeatureFlags.Mentions.defaultValue(aBuildMeta())) 2 else 1) - return awaitItem() - } - private fun TestScope.createMessagesPresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), matrixRoom: MatrixRoom = FakeMatrixRoom( @@ -993,83 +998,34 @@ class MessagesPresenterTest { navigator: FakeMessagesNavigator = FakeMessagesNavigator(), clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(), - permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), endPollAction: EndPollAction = FakeEndPollAction(), permalinkParser: PermalinkParser = FakePermalinkParser(), + messageComposerPresenter: Presenter = Presenter { + aMessageComposerState( + // Use TextEditorState.Markdown, so that we can request focus manually. + textEditorState = TextEditorState.Markdown(MarkdownTextEditorState(initialText = "", initialFocus = false)) + ) + }, + actionListEventSink: (ActionListEvents) -> Unit = {}, ): MessagesPresenter { - val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom) - val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) - val sessionPreferencesStore = InMemorySessionPreferencesStore() - val mentionSpanProvider = MentionSpanProvider(FakePermalinkParser()) - val messageComposerPresenter = MessageComposerPresenter( - appCoroutineScope = this, - room = matrixRoom, - mediaPickerProvider = FakePickerProvider(), - featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.NotificationSettings.key to true)), - sessionPreferencesStore = InMemorySessionPreferencesStore(), - localMediaFactory = FakeLocalMediaFactory(mockMediaUrl), - mediaSender = mediaSender, - snackbarDispatcher = SnackbarDispatcher(), - analyticsService = analyticsService, - messageComposerContext = DefaultMessageComposerContext(), - richTextEditorStateFactory = TestRichTextEditorStateFactory(), - roomAliasSuggestionsDataSource = FakeRoomAliasSuggestionsDataSource(), - permissionsPresenterFactory = permissionsPresenterFactory, - permalinkParser = FakePermalinkParser(), - permalinkBuilder = FakePermalinkBuilder(), - timelineController = TimelineController(matrixRoom), - draftService = FakeComposerDraftService(), - mentionSpanProvider = mentionSpanProvider, - pillificationHelper = FakeTextPillificationHelper(), - roomMemberProfilesCache = RoomMemberProfilesCache(), - suggestionsProcessor = SuggestionsProcessor(), - ).apply { - showTextFormatting = true - isTesting = true - } - val voiceMessageComposerPresenter = VoiceMessageComposerPresenter( - this, - FakeVoiceRecorder(), - analyticsService, - mediaSender, - player = VoiceMessageComposerPlayer(FakeMediaPlayer(), this), - messageComposerContext = FakeMessageComposerContext(), - permissionsPresenterFactory, - ) - val timelinePresenter = TimelinePresenter( - timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), - room = matrixRoom, - dispatchers = coroutineDispatchers, - appScope = this, - navigator = navigator, - redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), - endPollAction = endPollAction, - sendPollResponseAction = FakeSendPollResponseAction(), - sessionPreferencesStore = sessionPreferencesStore, - timelineItemIndexer = TimelineItemIndexer(), - timelineController = TimelineController(matrixRoom), - resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, - typingNotificationPresenter = { aTypingNotificationState() }, - ) val timelinePresenterFactory = object : TimelinePresenter.Factory { override fun create(navigator: MessagesNavigator): TimelinePresenter { - return timelinePresenter + return createTimelinePresenter( + endPollAction = endPollAction, + ) } } val featureFlagService = FakeFeatureFlagService() - val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() - val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) - val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) return MessagesPresenter( room = matrixRoom, composerPresenter = messageComposerPresenter, - voiceMessageComposerPresenter = voiceMessageComposerPresenter, + voiceMessageComposerPresenter = { aVoiceMessageComposerState() }, timelinePresenterFactory = timelinePresenterFactory, timelineProtectionPresenter = { aTimelineProtectionState() }, - actionListPresenterFactory = FakeActionListPresenter.Factory, - customReactionPresenter = customReactionPresenter, - reactionSummaryPresenter = reactionSummaryPresenter, - readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter, + actionListPresenterFactory = FakeActionListPresenter.Factory(actionListEventSink), + customReactionPresenter = { aCustomReactionState() }, + reactionSummaryPresenter = { aReactionSummaryState() }, + readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() }, pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() }, networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt index 468fd0620b..14f62a1daf 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt @@ -10,15 +10,15 @@ package io.element.android.features.messages.impl.actionlist import androidx.compose.runtime.Composable import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor -class FakeActionListPresenter : ActionListPresenter { - object Factory : ActionListPresenter.Factory { +class FakeActionListPresenter(private val eventSink: (ActionListEvents) -> Unit = {}) : ActionListPresenter { + class Factory(private val eventSink: (ActionListEvents) -> Unit = {}) : ActionListPresenter.Factory { override fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter { - return FakeActionListPresenter() + return FakeActionListPresenter(eventSink) } } @Composable override fun present(): ActionListState { - return anActionListState() + return anActionListState(eventSink = eventSink) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt index b8715568d1..1036788cbb 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -312,7 +312,7 @@ class PinnedMessagesListPresenterTest { timelineProvider = timelineProvider, timelineProtectionPresenter = { aTimelineProtectionState() }, snackbarDispatcher = SnackbarDispatcher(), - actionListPresenterFactory = FakeActionListPresenter.Factory, + actionListPresenterFactory = FakeActionListPresenter.Factory(), analyticsService = analyticsService, appCoroutineScope = this, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index a008feac24..74df8ee46c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -656,34 +656,34 @@ import kotlin.time.Duration.Companion.seconds private suspend fun ReceiveTurbine.awaitFirstItem(): T { return awaitItem() } - - private fun TestScope.createTimelinePresenter( - timeline: Timeline = FakeTimeline(), - room: FakeMatrixRoom = FakeMatrixRoom( - liveTimeline = timeline, - canUserSendMessageResult = { _, _ -> Result.success(true) } - ), - redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), - messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), - endPollAction: EndPollAction = FakeEndPollAction(), - sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), - sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), - timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), - ): TimelinePresenter { - return TimelinePresenter( - timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), - room = room, - dispatchers = testCoroutineDispatchers(), - appScope = this, - navigator = messagesNavigator, - redactedVoiceMessageManager = redactedVoiceMessageManager, - endPollAction = endPollAction, - sendPollResponseAction = sendPollResponseAction, - sessionPreferencesStore = sessionPreferencesStore, - timelineItemIndexer = timelineItemIndexer, - timelineController = TimelineController(room), - resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, - typingNotificationPresenter = { aTypingNotificationState() }, - ) - } +} + +internal fun TestScope.createTimelinePresenter( + timeline: Timeline = FakeTimeline(), + room: FakeMatrixRoom = FakeMatrixRoom( + liveTimeline = timeline, + canUserSendMessageResult = { _, _ -> Result.success(true) } + ), + redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), + messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), + endPollAction: EndPollAction = FakeEndPollAction(), + sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), + sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), + timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), +): TimelinePresenter { + return TimelinePresenter( + timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), + room = room, + dispatchers = testCoroutineDispatchers(), + appScope = this, + navigator = messagesNavigator, + redactedVoiceMessageManager = redactedVoiceMessageManager, + endPollAction = endPollAction, + sendPollResponseAction = sendPollResponseAction, + sessionPreferencesStore = sessionPreferencesStore, + timelineItemIndexer = timelineItemIndexer, + timelineController = TimelineController(room), + resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, + typingNotificationPresenter = { aTypingNotificationState() }, + ) } diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 7cd76442bf..5dff5a65d4 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -83,7 +83,6 @@ dependencies { testImplementation(projects.features.logout.test) testImplementation(projects.libraries.indicator.impl) testImplementation(projects.libraries.pushproviders.test) - testImplementation(projects.libraries.fullscreenintent.test) testImplementation(projects.features.logout.impl) testImplementation(projects.services.analytics.test) testImplementation(projects.services.toolbox.test) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenter.kt index c7d9c521fa..6056e53bf9 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenter.kt @@ -8,19 +8,19 @@ package io.element.android.features.preferences.impl.analytics import androidx.compose.runtime.Composable -import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter +import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState import io.element.android.libraries.architecture.Presenter import javax.inject.Inject class AnalyticsSettingsPresenter @Inject constructor( - private val analyticsPresenter: AnalyticsPreferencesPresenter, + private val analyticsPreferencesPresenter: Presenter, ) : Presenter { @Composable override fun present(): AnalyticsSettingsState { - val analyticsState = analyticsPresenter.present() + val analyticsPreferencesState = analyticsPreferencesPresenter.present() return AnalyticsSettingsState( - analyticsState = analyticsState, + analyticsPreferencesState = analyticsPreferencesState, ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsState.kt index f86e5aca5f..948369f8b8 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsState.kt @@ -11,5 +11,5 @@ import io.element.android.features.analytics.api.preferences.AnalyticsPreference // Do not use default value, so no member get forgotten in the presenters. data class AnalyticsSettingsState( - val analyticsState: AnalyticsPreferencesState, + val analyticsPreferencesState: AnalyticsPreferencesState, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsStateProvider.kt index 97f8a72785..fc58b7a255 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsStateProvider.kt @@ -18,5 +18,5 @@ open class AnalyticsSettingsStateProvider : PreviewParameterProvider, private val appPreferencesStore: AppPreferencesStore, private val buildMeta: BuildMeta, private val logoutUseCase: LogoutUseCase, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt index ecfe60e0f9..66574f4c28 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt @@ -22,7 +22,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingStateNoSuccess -import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsPresenter +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomNotificationMode @@ -47,7 +47,7 @@ class NotificationSettingsPresenter @Inject constructor( private val matrixClient: MatrixClient, private val pushService: PushService, private val systemNotificationsEnabledProvider: SystemNotificationsEnabledProvider, - private val fullScreenIntentPermissionsPresenter: FullScreenIntentPermissionsPresenter, + private val fullScreenIntentPermissionsPresenter: Presenter, ) : Presenter { @Composable override fun present(): NotificationSettingsState { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt index 25b6bdacc9..0b1fcf43a8 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState +import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.room.RoomNotificationMode import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -87,15 +88,3 @@ fun aInvalidNotificationSettingsState( fullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(), eventSink = eventSink, ) - -internal fun aFullScreenIntentPermissionsState( - permissionGranted: Boolean = true, - shouldDisplay: Boolean = false, - openFullScreenIntentSettings: () -> Unit = {}, - dismissFullScreenIntentBanner: () -> Unit = {}, -) = FullScreenIntentPermissionsState( - permissionGranted = permissionGranted, - shouldDisplayBanner = shouldDisplay, - openFullScreenIntentSettings = openFullScreenIntentSettings, - dismissFullScreenIntentBanner = dismissFullScreenIntentBanner, -) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index f1507c45f4..7429b3dc4e 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -16,7 +16,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import io.element.android.features.logout.api.direct.DirectLogoutPresenter +import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -42,7 +42,7 @@ class PreferencesRootPresenter @Inject constructor( private val snackbarDispatcher: SnackbarDispatcher, private val featureFlagService: FeatureFlagService, private val indicatorService: IndicatorService, - private val directLogoutPresenter: DirectLogoutPresenter, + private val directLogoutPresenter: Presenter, private val showDeveloperSettingsProvider: ShowDeveloperSettingsProvider, ) : Presenter { @Composable diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenterTest.kt index e621c383a3..c4203116a1 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenterTest.kt @@ -11,9 +11,7 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.analytics.impl.preferences.DefaultAnalyticsPreferencesPresenter -import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.features.analytics.api.preferences.aAnalyticsPreferencesState import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -25,15 +23,14 @@ class AnalyticsSettingsPresenterTest { @Test fun `present - initial state`() = runTest { - val analyticsPresenter = DefaultAnalyticsPreferencesPresenter(FakeAnalyticsService(), aBuildMeta()) val presenter = AnalyticsSettingsPresenter( - analyticsPresenter, + analyticsPreferencesPresenter = { aAnalyticsPreferencesState() }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.analyticsState.isEnabled).isFalse() + assertThat(initialState.analyticsPreferencesState.isEnabled).isFalse() } } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index 128f4aa705..c893de7f55 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -15,9 +15,7 @@ import io.element.android.appconfig.ElementCallConfig import io.element.android.features.logout.test.FakeLogoutUseCase import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase -import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter -import io.element.android.features.rageshake.test.rageshake.FakeRageShake -import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore +import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType @@ -54,7 +52,7 @@ class DeveloperSettingsPresenterTest { val loadedState = awaitItem() assertThat(loadedState.rageshakeState.isEnabled).isFalse() assertThat(loadedState.rageshakeState.isSupported).isTrue() - assertThat(loadedState.rageshakeState.sensitivity).isEqualTo(1.0f) + assertThat(loadedState.rageshakeState.sensitivity).isEqualTo(0.3f) cancelAndIgnoreRemainingEvents() } } @@ -105,9 +103,8 @@ class DeveloperSettingsPresenterTest { @Test fun `present - clear cache`() = runTest { - val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()) val clearCacheUseCase = FakeClearCacheUseCase() - val presenter = createDeveloperSettingsPresenter(clearCacheUseCase = clearCacheUseCase, rageshakePresenter = rageshakePresenter) + val presenter = createDeveloperSettingsPresenter(clearCacheUseCase = clearCacheUseCase) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -202,7 +199,6 @@ class DeveloperSettingsPresenterTest { featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(), clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(), - rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()), preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), buildMeta: BuildMeta = aBuildMeta(), logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }) @@ -211,7 +207,7 @@ class DeveloperSettingsPresenterTest { featureFlagService = featureFlagService, computeCacheSizeUseCase = cacheSizeUseCase, clearCacheUseCase = clearCacheUseCase, - rageshakePresenter = rageshakePresenter, + rageshakePresenter = { aRageshakePreferencesState() }, appPreferencesStore = preferencesStore, buildMeta = buildMeta, logoutUseCase = logoutUseCase, diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt index 150b761432..96755a9717 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt @@ -12,7 +12,8 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.fullscreenintent.test.FakeFullScreenIntentPermissionsPresenter +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState +import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.test.A_THROWABLE @@ -263,12 +264,11 @@ class NotificationSettingsPresenterTest { @Test fun `present - RefreshSystemNotificationsEnabled also refreshes fullScreenIntentState`() = runTest { - val fullScreenIntentPermissionsPresenter = FakeFullScreenIntentPermissionsPresenter().apply { - state = state.copy(permissionGranted = false) - } + var lambdaResult = aFullScreenIntentPermissionsState(permissionGranted = false) + val fullScreenIntentPermissionsStateLambda = { lambdaResult } val presenter = createNotificationSettingsPresenter( pushService = createFakePushService(), - fullScreenIntentPermissionsPresenter = fullScreenIntentPermissionsPresenter, + fullScreenIntentPermissionsStateLambda = fullScreenIntentPermissionsStateLambda, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -277,7 +277,7 @@ class NotificationSettingsPresenterTest { assertThat(initialState.fullScreenIntentPermissionsState.permissionGranted).isFalse() // Change the notification settings - fullScreenIntentPermissionsPresenter.state = fullScreenIntentPermissionsPresenter.state.copy(permissionGranted = true) + lambdaResult = lambdaResult.copy(permissionGranted = true) // Check it's not changed unless we refresh expectNoEvents() @@ -336,7 +336,7 @@ class NotificationSettingsPresenterTest { private fun createNotificationSettingsPresenter( notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), pushService: PushService = FakePushService(), - fullScreenIntentPermissionsPresenter: FakeFullScreenIntentPermissionsPresenter = FakeFullScreenIntentPermissionsPresenter() + fullScreenIntentPermissionsStateLambda: () -> FullScreenIntentPermissionsState = { aFullScreenIntentPermissionsState() }, ): NotificationSettingsPresenter { val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) return NotificationSettingsPresenter( @@ -345,7 +345,7 @@ class NotificationSettingsPresenterTest { matrixClient = matrixClient, pushService = pushService, systemNotificationsEnabledProvider = FakeSystemNotificationsEnabledProvider(), - fullScreenIntentPermissionsPresenter = fullScreenIntentPermissionsPresenter, + fullScreenIntentPermissionsPresenter = { fullScreenIntentPermissionsStateLambda() }, ) } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index 774d52076a..5c2bc63add 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -7,16 +7,13 @@ package io.element.android.features.preferences.impl.root -import androidx.compose.runtime.Composable import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.logout.api.direct.DirectLogoutPresenter -import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider -import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.featureflag.test.FakeFeatureFlagService @@ -38,12 +35,6 @@ class PreferencesRootPresenterTest { @get:Rule val warmUpRule = WarmUpRule() - private val aDirectLogoutState = DirectLogoutState( - canDoDirectSignOut = true, - logoutAction = AsyncAction.Uninitialized, - eventSink = {}, - ) - @Test fun `present - initial state`() = runTest { val matrixClient = FakeMatrixClient(canDeactivateAccountResult = { true }) @@ -78,7 +69,7 @@ class PreferencesRootPresenterTest { assertThat(loadedState.showLockScreenSettings).isTrue() assertThat(loadedState.showNotificationSettings).isTrue() assertThat(loadedState.canDeactivateAccount).isTrue() - assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState) + assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState()) assertThat(loadedState.snackbarMessage).isNull() } } @@ -148,10 +139,7 @@ class PreferencesRootPresenterTest { sessionVerificationService = sessionVerificationService, encryptionService = FakeEncryptionService(), ), - directLogoutPresenter = object : DirectLogoutPresenter { - @Composable - override fun present() = aDirectLogoutState - }, + directLogoutPresenter = { aDirectLogoutState() }, showDeveloperSettingsProvider = showDeveloperSettingsProvider, ) } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeModule.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeModule.kt new file mode 100644 index 0000000000..393a9913ae --- /dev/null +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.rageshake.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter +import io.element.android.features.rageshake.api.crash.CrashDetectionState +import io.element.android.features.rageshake.api.detection.RageshakeDetectionPresenter +import io.element.android.features.rageshake.api.detection.RageshakeDetectionState +import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter +import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.AppScope + +@ContributesTo(AppScope::class) +@Module +interface RageshakeModule { + @Binds + fun bindRageshakePreferencesPresenter(presenter: RageshakePreferencesPresenter): Presenter + + @Binds + fun bindRageshakeDetectionPresenter(presenter: RageshakeDetectionPresenter): Presenter + + @Binds + fun bindCrashDetectionPresenter(presenter: CrashDetectionPresenter): Presenter +} diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index c8bf9d36f1..42f27963f3 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -64,7 +64,6 @@ dependencies { testImplementation(projects.libraries.usersearch.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.tests.testutils) - testImplementation(projects.features.leaveroom.test) testImplementation(projects.features.createroom.test) testImplementation(projects.services.analytics.test) testImplementation(libs.androidx.compose.ui.test.junit) 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 ee63be7643..eccc8dd9d2 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 @@ -19,7 +19,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import im.vector.app.features.analytics.plan.Interaction 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.messages.api.pinned.IsPinnedMessagesFeatureEnabled import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.architecture.Presenter @@ -56,7 +56,7 @@ class RoomDetailsPresenter @Inject constructor( private val featureFlagService: FeatureFlagService, private val notificationSettingsService: NotificationSettingsService, private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory, - private val leaveRoomPresenter: LeaveRoomPresenter, + private val leaveRoomPresenter: Presenter, private val dispatchers: CoroutineDispatchers, private val analyticsService: AnalyticsService, private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomDetailsModule.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomDetailsModule.kt new file mode 100644 index 0000000000..0cabbdd09c --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomDetailsModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.roomdetails.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter +import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.RoomScope + +@Module +@ContributesTo(RoomScope::class) +interface RoomDetailsModule { + @Binds + fun bindRoomMembersModerationPresenter(presenter: RoomMembersModerationPresenter): Presenter +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 4ef746b0e7..c6fd2bc0b8 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -21,7 +21,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter +import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -40,7 +40,7 @@ class RoomMemberListPresenter @AssistedInject constructor( private val room: MatrixRoom, private val roomMemberListDataSource: RoomMemberListDataSource, private val coroutineDispatchers: CoroutineDispatchers, - private val roomMembersModerationPresenter: RoomMembersModerationPresenter, + private val roomMembersModerationPresenter: Presenter, @Assisted private val navigator: RoomMemberListNavigator, ) : Presenter { @AssistedFactory @@ -136,7 +136,7 @@ class RoomMemberListPresenter @AssistedInject constructor( is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query is RoomMemberListEvents.RoomMemberSelected -> coroutineScope.launch { - if (roomMembersModerationPresenter.canDisplayModerationActions()) { + if (roomModerationState.canDisplayModerationActions) { roomModerationState.eventSink(RoomMembersModerationEvents.SelectRoomMember(event.roomMember)) } else { navigator.openRoomMemberDetails(event.roomMember.userId) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/DefaultRoomMembersModerationPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/DefaultRoomMembersModerationPresenter.kt deleted file mode 100644 index e2081f6c6f..0000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/DefaultRoomMembersModerationPresenter.kt +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.roomdetails.impl.members.moderation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import com.squareup.anvil.annotations.ContributesBinding -import im.vector.app.features.analytics.plan.RoomModeration -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.runUpdatingState -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.core.extensions.finally -import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.RoomMembershipState -import io.element.android.libraries.matrix.api.room.isDm -import io.element.android.libraries.matrix.api.room.powerlevels.canBan -import io.element.android.libraries.matrix.api.room.powerlevels.canKick -import io.element.android.services.analytics.api.AnalyticsService -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ContributesBinding(RoomScope::class) -class DefaultRoomMembersModerationPresenter @Inject constructor( - private val room: MatrixRoom, - private val dispatchers: CoroutineDispatchers, - private val analyticsService: AnalyticsService, -) : RoomMembersModerationPresenter { - private var selectedMember by mutableStateOf(null) - - private suspend fun canBan() = room.canBan().getOrDefault(false) - private suspend fun canKick() = room.canKick().getOrDefault(false) - - override suspend fun canDisplayModerationActions(): Boolean { - return !room.isDm && (canBan() || canKick()) - } - - @Composable - override fun present(): RoomMembersModerationState { - val coroutineScope = rememberCoroutineScope() - var moderationActions by remember { mutableStateOf(persistentListOf()) } - - val kickUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } - val banUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } - val unbanUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } - - val canDisplayBannedUsers by produceState(initialValue = false) { - value = !room.isDm && canBan() - } - - fun handleEvent(event: RoomMembersModerationEvents) { - when (event) { - is RoomMembersModerationEvents.SelectRoomMember -> { - coroutineScope.launch { - selectedMember = event.roomMember - if (event.roomMember.membership == RoomMembershipState.BAN && canBan()) { - unbanUserAsyncAction.value = AsyncAction.Confirming - } else { - moderationActions = buildList { - add(ModerationAction.DisplayProfile(event.roomMember.userId)) - val currentUserMemberPowerLevel = room.userRole(room.sessionId).getOrDefault(RoomMember.Role.USER).powerLevel - if (currentUserMemberPowerLevel > event.roomMember.powerLevel) { - if (canKick()) { - add(ModerationAction.KickUser(event.roomMember.userId)) - } - if (canBan()) { - add(ModerationAction.BanUser(event.roomMember.userId)) - } - } - }.toPersistentList() - } - } - } - is RoomMembersModerationEvents.KickUser -> { - moderationActions = persistentListOf() - selectedMember?.let { - coroutineScope.kickUser(it.userId, kickUserAsyncAction) - } - } - is RoomMembersModerationEvents.BanUser -> { - if (banUserAsyncAction.value.isConfirming()) { - moderationActions = persistentListOf() - selectedMember?.let { - coroutineScope.banUser(it.userId, banUserAsyncAction) - } - } else { - banUserAsyncAction.value = AsyncAction.Confirming - } - } - is RoomMembersModerationEvents.UnbanUser -> { - if (unbanUserAsyncAction.value.isConfirming()) { - moderationActions = persistentListOf() - selectedMember?.let { - coroutineScope.unbanUser(it.userId, unbanUserAsyncAction) - } - } else { - unbanUserAsyncAction.value = AsyncAction.Confirming - } - } - is RoomMembersModerationEvents.Reset -> { - selectedMember = null - moderationActions = persistentListOf() - kickUserAsyncAction.value = AsyncAction.Uninitialized - banUserAsyncAction.value = AsyncAction.Uninitialized - unbanUserAsyncAction.value = AsyncAction.Uninitialized - } - } - } - - return RoomMembersModerationState( - selectedRoomMember = selectedMember, - actions = moderationActions, - kickUserAsyncAction = kickUserAsyncAction.value, - banUserAsyncAction = banUserAsyncAction.value, - unbanUserAsyncAction = unbanUserAsyncAction.value, - canDisplayBannedUsers = canDisplayBannedUsers, - eventSink = { handleEvent(it) }, - ) - } - - private fun CoroutineScope.kickUser( - userId: UserId, - kickUserAction: MutableState>, - ) = runActionAndWaitForMembershipChange(kickUserAction) { - analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember)) - room.kickUser(userId).finally { selectedMember = null } - } - - private fun CoroutineScope.banUser( - userId: UserId, - banUserAction: MutableState>, - ) = runActionAndWaitForMembershipChange(banUserAction) { - analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember)) - room.banUser(userId).finally { selectedMember = null } - } - - private fun CoroutineScope.unbanUser( - userId: UserId, - unbanUserAction: MutableState>, - ) = runActionAndWaitForMembershipChange(unbanUserAction) { - analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember)) - room.unbanUser(userId).finally { selectedMember = null } - } - - private fun CoroutineScope.runActionAndWaitForMembershipChange(action: MutableState>, block: suspend () -> Result) { - launch(dispatchers.io) { - action.runUpdatingState { - val result = block() - if (result.isSuccess) { - room.membersStateFlow.drop(1).take(1) - } - result - } - } - } -} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt index cdc3ecce03..0b9e744dd2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt @@ -7,20 +7,179 @@ package io.element.android.features.roomdetails.impl.members.moderation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import im.vector.app.features.analytics.plan.RoomModeration import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.finally +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.room.isDm +import io.element.android.libraries.matrix.api.room.powerlevels.canBan +import io.element.android.libraries.matrix.api.room.powerlevels.canKick +import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import javax.inject.Inject -interface RoomMembersModerationPresenter : Presenter { - suspend fun canDisplayModerationActions(): Boolean +class RoomMembersModerationPresenter @Inject constructor( + private val room: MatrixRoom, + private val dispatchers: CoroutineDispatchers, + private val analyticsService: AnalyticsService, +) : Presenter { + private var selectedMember by mutableStateOf(null) - fun dummyState() = RoomMembersModerationState( - selectedRoomMember = null, - actions = persistentListOf(), - kickUserAsyncAction = AsyncAction.Uninitialized, - banUserAsyncAction = AsyncAction.Uninitialized, - unbanUserAsyncAction = AsyncAction.Uninitialized, - canDisplayBannedUsers = false, - eventSink = {} - ) + private suspend fun canBan() = room.canBan().getOrDefault(false) + private suspend fun canKick() = room.canKick().getOrDefault(false) + + @Composable + override fun present(): RoomMembersModerationState { + val coroutineScope = rememberCoroutineScope() + var moderationActions by remember { mutableStateOf(persistentListOf()) } + + val syncUpdateFlow = room.syncUpdateFlow.collectAsState() + val canDisplayModerationActions by produceState( + initialValue = false, + key1 = syncUpdateFlow.value + ) { + value = !room.isDm && (canBan() || canKick()) + } + val kickUserAsyncAction = + remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } + val banUserAsyncAction = + remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } + val unbanUserAsyncAction = + remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } + + val canDisplayBannedUsers by produceState(initialValue = false) { + value = !room.isDm && canBan() + } + + fun handleEvent(event: RoomMembersModerationEvents) { + when (event) { + is RoomMembersModerationEvents.SelectRoomMember -> { + coroutineScope.launch { + selectedMember = event.roomMember + if (event.roomMember.membership == RoomMembershipState.BAN && canBan()) { + unbanUserAsyncAction.value = AsyncAction.Confirming + } else { + moderationActions = buildList { + add(ModerationAction.DisplayProfile(event.roomMember.userId)) + val currentUserMemberPowerLevel = room.userRole(room.sessionId) + .getOrDefault(RoomMember.Role.USER) + .powerLevel + if (currentUserMemberPowerLevel > event.roomMember.powerLevel) { + if (canKick()) { + add(ModerationAction.KickUser(event.roomMember.userId)) + } + if (canBan()) { + add(ModerationAction.BanUser(event.roomMember.userId)) + } + } + }.toPersistentList() + } + } + } + is RoomMembersModerationEvents.KickUser -> { + moderationActions = persistentListOf() + selectedMember?.let { + coroutineScope.kickUser(it.userId, kickUserAsyncAction) + } + } + is RoomMembersModerationEvents.BanUser -> { + if (banUserAsyncAction.value.isConfirming()) { + moderationActions = persistentListOf() + selectedMember?.let { + coroutineScope.banUser(it.userId, banUserAsyncAction) + } + } else { + banUserAsyncAction.value = AsyncAction.Confirming + } + } + is RoomMembersModerationEvents.UnbanUser -> { + if (unbanUserAsyncAction.value.isConfirming()) { + moderationActions = persistentListOf() + selectedMember?.let { + coroutineScope.unbanUser(it.userId, unbanUserAsyncAction) + } + } else { + unbanUserAsyncAction.value = AsyncAction.Confirming + } + } + is RoomMembersModerationEvents.Reset -> { + selectedMember = null + moderationActions = persistentListOf() + kickUserAsyncAction.value = AsyncAction.Uninitialized + banUserAsyncAction.value = AsyncAction.Uninitialized + unbanUserAsyncAction.value = AsyncAction.Uninitialized + } + } + } + + return RoomMembersModerationState( + canDisplayModerationActions = canDisplayModerationActions, + selectedRoomMember = selectedMember, + actions = moderationActions, + kickUserAsyncAction = kickUserAsyncAction.value, + banUserAsyncAction = banUserAsyncAction.value, + unbanUserAsyncAction = unbanUserAsyncAction.value, + canDisplayBannedUsers = canDisplayBannedUsers, + eventSink = { handleEvent(it) }, + ) + } + + private fun CoroutineScope.kickUser( + userId: UserId, + kickUserAction: MutableState>, + ) = runActionAndWaitForMembershipChange(kickUserAction) { + analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember)) + room.kickUser(userId).finally { selectedMember = null } + } + + private fun CoroutineScope.banUser( + userId: UserId, + banUserAction: MutableState>, + ) = runActionAndWaitForMembershipChange(banUserAction) { + analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember)) + room.banUser(userId).finally { selectedMember = null } + } + + private fun CoroutineScope.unbanUser( + userId: UserId, + unbanUserAction: MutableState>, + ) = runActionAndWaitForMembershipChange(unbanUserAction) { + analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember)) + room.unbanUser(userId).finally { selectedMember = null } + } + + private fun CoroutineScope.runActionAndWaitForMembershipChange( + action: MutableState>, + block: suspend () -> Result + ) { + launch(dispatchers.io) { + action.runUpdatingState { + val result = block() + if (result.isSuccess) { + room.membersStateFlow.drop(1).take(1) + } + result + } + } + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt index c906ebd561..18db706438 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt @@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember import kotlinx.collections.immutable.ImmutableList data class RoomMembersModerationState( + val canDisplayModerationActions: Boolean, val selectedRoomMember: RoomMember?, val actions: ImmutableList, val kickUserAsyncAction: AsyncAction, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStatePreviewProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStatePreviewProvider.kt index 18030139e5..d7fb509995 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStatePreviewProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStatePreviewProvider.kt @@ -71,6 +71,7 @@ class RoomMembersModerationStatePreviewProvider : PreviewParameterProvider = emptyList(), kickUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, @@ -79,6 +80,7 @@ fun aRoomMembersModerationState( canDisplayBannedUsers: Boolean = false, eventSink: (RoomMembersModerationEvents) -> Unit = {}, ) = RoomMembersModerationState( + canDisplayModerationActions = canDisplayModerationActions, selectedRoomMember = selectedRoomMember, actions = actions.toPersistentList(), kickUserAsyncAction = kickUserAsyncAction, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTest.kt index 84f6f5f341..73eee2092e 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTest.kt @@ -16,8 +16,8 @@ import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.createroom.test.FakeStartDMAction import io.element.android.features.leaveroom.api.LeaveRoomEvent -import io.element.android.features.leaveroom.api.LeaveRoomPresenter -import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter +import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.roomdetails.impl.RoomDetailsEvent import io.element.android.features.roomdetails.impl.RoomDetailsPresenter import io.element.android.features.roomdetails.impl.RoomDetailsState @@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.FakeLifecycleOwner import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate @@ -72,7 +73,7 @@ class RoomDetailsPresenterTest { private fun TestScope.createRoomDetailsPresenter( room: MatrixRoom = aMatrixRoom(), - leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(), + leaveRoomState: LeaveRoomState = aLeaveRoomState(), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), analyticsService: AnalyticsService = FakeAnalyticsService(), @@ -93,7 +94,7 @@ class RoomDetailsPresenterTest { featureFlagService = featureFlagService, notificationSettingsService = matrixClient.notificationSettingsService(), roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory, - leaveRoomPresenter = leaveRoomPresenter, + leaveRoomPresenter = { leaveRoomState }, dispatchers = dispatchers, isPinnedMessagesFeatureEnabled = { isPinnedMessagesFeatureEnabled }, analyticsService = analyticsService, @@ -475,7 +476,7 @@ class RoomDetailsPresenterTest { @Test fun `present - leave room event is passed on to leave room presenter`() = runTest { - val leaveRoomPresenter = FakeLeaveRoomPresenter() + val leaveRoomEventRecorder = EventsRecorder() val room = aMatrixRoom( canInviteResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) }, @@ -483,25 +484,18 @@ class RoomDetailsPresenterTest { ) val presenter = createRoomDetailsPresenter( room = room, - leaveRoomPresenter = leaveRoomPresenter, + leaveRoomState = aLeaveRoomState(eventSink = leaveRoomEventRecorder), dispatchers = testCoroutineDispatchers() ) presenter.test { awaitItem().eventSink(RoomDetailsEvent.LeaveRoom) - - assertThat(leaveRoomPresenter.events).contains( - LeaveRoomEvent.ShowConfirmation( - room.roomId - ) - ) - + leaveRoomEventRecorder.assertSingle(LeaveRoomEvent.ShowConfirmation(room.roomId)) cancelAndIgnoreRemainingEvents() } } @Test fun `present - notification mode changes`() = runTest { - val leaveRoomPresenter = FakeLeaveRoomPresenter() val notificationSettingsService = FakeNotificationSettingsService() val room = aMatrixRoom( notificationSettingsService = notificationSettingsService, @@ -511,7 +505,6 @@ class RoomDetailsPresenterTest { ) val presenter = createRoomDetailsPresenter( room = room, - leaveRoomPresenter = leaveRoomPresenter, notificationSettingsService = notificationSettingsService, ) presenter.test { diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTest.kt index 6ba283d7ef..e9414f90be 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTest.kt @@ -19,8 +19,8 @@ import io.element.android.features.roomdetails.impl.members.aRoomMemberList import io.element.android.features.roomdetails.impl.members.aVictor import io.element.android.features.roomdetails.impl.members.aWalter import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents +import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState -import io.element.android.features.roomdetails.members.moderation.FakeRoomMembersModerationPresenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.UserId @@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -194,9 +195,9 @@ class RoomMemberListPresenterTest { @Test fun `present - RoomMemberSelected by default opens the room member details through the navigator`() = runTest { val navigator = FakeRoomMemberListNavigator() - val moderationPresenter = FakeRoomMembersModerationPresenter(canDisplayModerationActions = false) + val roomMembersModerationStateLambda = { aRoomMembersModerationState(canDisplayModerationActions = false) } val presenter = createPresenter( - moderationPresenter = moderationPresenter, + roomMembersModerationStateLambda = roomMembersModerationStateLambda, navigator = navigator, matrixRoom = FakeMatrixRoom( updateMembersResult = { Result.success(Unit) }, @@ -215,17 +216,15 @@ class RoomMemberListPresenterTest { @Test fun `present - RoomMemberSelected will open the moderation options if the current user can use them`() = runTest { val navigator = FakeRoomMemberListNavigator() - var selectRoomMemberCallCounts = 0 - val capturingState = aRoomMembersModerationState(eventSink = { event -> - if (event is RoomMembersModerationEvents.SelectRoomMember) { - selectRoomMemberCallCounts++ - } - }) - val moderationPresenter = FakeRoomMembersModerationPresenter(canDisplayModerationActions = true).apply { - givenState(capturingState) + val eventsRecorder = EventsRecorder() + val roomMembersModerationStateLambda = { + aRoomMembersModerationState( + canDisplayModerationActions = true, + eventSink = eventsRecorder, + ) } val presenter = createPresenter( - moderationPresenter = moderationPresenter, + roomMembersModerationStateLambda = roomMembersModerationStateLambda, navigator = navigator, matrixRoom = FakeMatrixRoom( updateMembersResult = { Result.success(Unit) }, @@ -237,7 +236,7 @@ class RoomMemberListPresenterTest { }.test { skipItems(1) awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor())) - assertThat(selectRoomMemberCallCounts).isEqualTo(1) + eventsRecorder.assertSingle(RoomMembersModerationEvents.SelectRoomMember(aVictor())) } } } @@ -266,12 +265,12 @@ private fun TestScope.createPresenter( updateMembersResult = { Result.success(Unit) } ), roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers), - moderationPresenter: FakeRoomMembersModerationPresenter = FakeRoomMembersModerationPresenter(), + roomMembersModerationStateLambda: () -> RoomMembersModerationState = { aRoomMembersModerationState() }, navigator: RoomMemberListNavigator = object : RoomMemberListNavigator {} ) = RoomMemberListPresenter( room = matrixRoom, roomMemberListDataSource = roomMemberListDataSource, coroutineDispatchers = coroutineDispatchers, - roomMembersModerationPresenter = moderationPresenter, + roomMembersModerationPresenter = { roomMembersModerationStateLambda() }, navigator = navigator ) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/FakeRoomMembersModerationPresenter.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/FakeRoomMembersModerationPresenter.kt deleted file mode 100644 index 96745130b7..0000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/FakeRoomMembersModerationPresenter.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.roomdetails.members.moderation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState - -class FakeRoomMembersModerationPresenter( - private val canDisplayModerationActions: Boolean = true, -) : RoomMembersModerationPresenter { - private var state by mutableStateOf(dummyState()) - - override suspend fun canDisplayModerationActions(): Boolean { - return canDisplayModerationActions - } - - @Composable - override fun present(): RoomMembersModerationState { - return state - } - - fun givenState(state: RoomMembersModerationState) { - this.state = state - } -} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/DefaultRoomMembersModerationPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt similarity index 89% rename from features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/DefaultRoomMembersModerationPresenterTest.kt rename to features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt index 926ca659d5..c2acd0a7de 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/DefaultRoomMembersModerationPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/RoomMembersModerationPresenterTest.kt @@ -14,9 +14,9 @@ import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.RoomModeration import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.aVictor -import io.element.android.features.roomdetails.impl.members.moderation.DefaultRoomMembersModerationPresenter import io.element.android.features.roomdetails.impl.members.moderation.ModerationAction import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents +import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState @@ -27,20 +27,23 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test -class DefaultRoomMembersModerationPresenterTest { +class RoomMembersModerationPresenterTest { @Test fun `canDisplayModerationActions - when room is DM is false`() = runTest { val room = FakeMatrixRoom(isDirect = true, isPublic = true, activeMemberCount = 2).apply { givenRoomInfo(aRoomInfo(isDirect = true, isPublic = false, activeMembersCount = 2)) } - val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) - assertThat(presenter.canDisplayModerationActions()).isFalse() + val presenter = createRoomMembersModerationPresenter(matrixRoom = room) + presenter.test { + assertThat(awaitItem().canDisplayModerationActions).isFalse() + } } @Test @@ -51,8 +54,11 @@ class DefaultRoomMembersModerationPresenterTest { canKickResult = { Result.success(true) }, canBanResult = { Result.success(true) }, ) - val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) - assertThat(presenter.canDisplayModerationActions()).isTrue() + val presenter = createRoomMembersModerationPresenter(matrixRoom = room) + presenter.test { + skipItems(1) + assertThat(awaitItem().canDisplayModerationActions).isTrue() + } } @Test @@ -62,8 +68,11 @@ class DefaultRoomMembersModerationPresenterTest { activeMemberCount = 10, canBanResult = { Result.success(true) }, ) - val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) - assertThat(presenter.canDisplayModerationActions()).isTrue() + val presenter = createRoomMembersModerationPresenter(matrixRoom = room) + presenter.test { + skipItems(1) + assertThat(awaitItem().canDisplayModerationActions).isTrue() + } } @Test @@ -74,7 +83,7 @@ class DefaultRoomMembersModerationPresenterTest { userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, ) val selectedMember = aVictor() - val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + val presenter = createRoomMembersModerationPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -101,7 +110,7 @@ class DefaultRoomMembersModerationPresenterTest { userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, ) val selectedMember = aRoomMember(A_USER_ID_2, powerLevel = 100L) - val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + val presenter = createRoomMembersModerationPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -125,7 +134,7 @@ class DefaultRoomMembersModerationPresenterTest { canBanResult = { Result.success(true) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, ) - val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + val presenter = createRoomMembersModerationPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -148,7 +157,7 @@ class DefaultRoomMembersModerationPresenterTest { kickUserResult = { _, _ -> Result.success(Unit) }, ) val selectedMember = aVictor() - val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) + val presenter = createRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -176,7 +185,7 @@ class DefaultRoomMembersModerationPresenterTest { banUserResult = { _, _ -> Result.success(Unit) }, ) val selectedMember = aVictor() - val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) + val presenter = createRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -211,7 +220,7 @@ class DefaultRoomMembersModerationPresenterTest { ).apply { givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(selectedMember))) } - val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) + val presenter = createRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -237,7 +246,7 @@ class DefaultRoomMembersModerationPresenterTest { canBanResult = { Result.success(true) }, userRoleResult = { Result.success(RoomMember.Role.USER) }, ) - val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + val presenter = createRoomMembersModerationPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -261,7 +270,7 @@ class DefaultRoomMembersModerationPresenterTest { unBanUserResult = { _, _ -> Result.failure(Throwable("Eek")) }, userRoleResult = { Result.success(RoomMember.Role.USER) }, ) - val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + val presenter = createRoomMembersModerationPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -301,12 +310,12 @@ class DefaultRoomMembersModerationPresenterTest { } } - private fun TestScope.createDefaultRoomMembersModerationPresenter( + private fun TestScope.createRoomMembersModerationPresenter( matrixRoom: FakeMatrixRoom = FakeMatrixRoom(), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(), - ): DefaultRoomMembersModerationPresenter { - return DefaultRoomMembersModerationPresenter( + ): RoomMembersModerationPresenter { + return RoomMembersModerationPresenter( room = matrixRoom, dispatchers = dispatchers, analyticsService = analyticsService, diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index d6b30a2b17..cb9c8670d0 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -65,7 +65,6 @@ dependencies { testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.libraries.eventformatter.test) testImplementation(projects.libraries.indicator.impl) - testImplementation(projects.libraries.fullscreenintent.test) testImplementation(projects.libraries.permissions.noop) testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.preferences.test) @@ -75,5 +74,4 @@ dependencies { testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.logout.test) testImplementation(projects.tests.testutils) - testImplementation(projects.features.leaveroom.test) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt index 6656ca8231..8b216f3854 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt @@ -10,6 +10,7 @@ package io.element.android.features.roomlist.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState +import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -37,15 +38,3 @@ internal fun aRoomsContentState( internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16) internal fun anEmptyContentState() = RoomListContentState.Empty - -internal fun aFullScreenIntentPermissionsState( - permissionGranted: Boolean = true, - shouldDisplay: Boolean = false, - openFullScreenIntentSettings: () -> Unit = {}, - dismissFullScreenIntentBanner: () -> Unit = {}, -) = FullScreenIntentPermissionsState( - permissionGranted = permissionGranted, - shouldDisplayBanner = shouldDisplay, - openFullScreenIntentSettings = openFullScreenIntentSettings, - dismissFullScreenIntentBanner = dismissFullScreenIntentBanner, -) 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 429bfbb698..8b034ad181 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,8 +28,8 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.InviteData import io.element.android.features.leaveroom.api.LeaveRoomEvent -import io.element.android.features.leaveroom.api.LeaveRoomPresenter -import io.element.android.features.logout.api.direct.DirectLogoutPresenter +import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.roomlist.impl.datasource.RoomListDataSource @@ -44,7 +44,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsPresenter +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -80,7 +80,7 @@ class RoomListPresenter @Inject constructor( private val client: MatrixClient, private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, - private val leaveRoomPresenter: LeaveRoomPresenter, + private val leaveRoomPresenter: Presenter, private val roomListDataSource: RoomListDataSource, private val featureFlagService: FeatureFlagService, private val indicatorService: IndicatorService, @@ -89,9 +89,9 @@ class RoomListPresenter @Inject constructor( private val sessionPreferencesStore: SessionPreferencesStore, private val analyticsService: AnalyticsService, private val acceptDeclineInvitePresenter: Presenter, - private val fullScreenIntentPermissionsPresenter: FullScreenIntentPermissionsPresenter, + private val fullScreenIntentPermissionsPresenter: Presenter, private val notificationCleaner: NotificationCleaner, - private val logoutPresenter: DirectLogoutPresenter, + private val logoutPresenter: Presenter, ) : Presenter { private val encryptionService: EncryptionService = client.encryptionService() private val syncService: SyncService = client.syncService() diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/FullScreenIntentPermissionBanner.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/FullScreenIntentPermissionBanner.kt index 662cca8ca9..73c6210b77 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/FullScreenIntentPermissionBanner.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/FullScreenIntentPermissionBanner.kt @@ -10,11 +10,11 @@ package io.element.android.features.roomlist.impl.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import io.element.android.features.roomlist.impl.R -import io.element.android.features.roomlist.impl.aFullScreenIntentPermissionsState import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState +import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState @Composable fun FullScreenIntentPermissionBanner(state: FullScreenIntentPermissionsState) { diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt index 8264263760..91242df095 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt @@ -7,7 +7,6 @@ package io.element.android.features.roomlist.impl -import androidx.compose.runtime.Composable import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test @@ -17,9 +16,8 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.anAcceptDeclineInviteState import io.element.android.features.leaveroom.api.LeaveRoomEvent -import io.element.android.features.leaveroom.api.LeaveRoomPresenter -import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter -import io.element.android.features.logout.api.direct.DirectLogoutPresenter +import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor @@ -40,7 +38,7 @@ import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.fullscreenintent.test.FakeFullScreenIntentPermissionsPresenter +import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.indicator.impl.DefaultIndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -84,10 +82,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy @@ -102,7 +97,6 @@ class RoomListPresenterTest { @Test fun `present - should start with no user and then load user with success`() = runTest { - val scope = CoroutineScope(coroutineContext + SupervisorJob()) val matrixClient = FakeMatrixClient( userDisplayName = null, userAvatarUrl = null, @@ -110,7 +104,6 @@ class RoomListPresenterTest { matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL))) val presenter = createRoomListPresenter( client = matrixClient, - coroutineScope = scope, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -122,13 +115,11 @@ class RoomListPresenterTest { assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME) assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(withUserState.showAvatarIndicator).isTrue() - scope.cancel() } } @Test fun `present - show avatar indicator`() = runTest { - val scope = CoroutineScope(coroutineContext + SupervisorJob()) val encryptionService = FakeEncryptionService() val sessionVerificationService = FakeSessionVerificationService() val matrixClient = FakeMatrixClient( @@ -137,7 +128,6 @@ class RoomListPresenterTest { ) val presenter = createRoomListPresenter( client = matrixClient, - coroutineScope = scope ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -148,7 +138,6 @@ class RoomListPresenterTest { encryptionService.emitBackupState(BackupState.ENABLED) val finalState = awaitItem() assertThat(finalState.showAvatarIndicator).isFalse() - scope.cancel() } } @@ -159,8 +148,7 @@ class RoomListPresenterTest { userAvatarUrl = null, ) matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.failure(AN_EXCEPTION)) - val scope = CoroutineScope(coroutineContext + SupervisorJob()) - val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) + val presenter = createRoomListPresenter(client = matrixClient) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -176,8 +164,7 @@ class RoomListPresenterTest { val matrixClient = FakeMatrixClient( roomListService = roomListService ) - val scope = CoroutineScope(coroutineContext + SupervisorJob()) - val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) + val presenter = createRoomListPresenter(client = matrixClient) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -202,13 +189,11 @@ class RoomListPresenterTest { ) ) cancelAndIgnoreRemainingEvents() - scope.cancel() } } @Test fun `present - handle DismissRequestVerificationPrompt`() = runTest { - val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val roomListService = FakeRoomListService().apply { postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) } @@ -218,7 +203,6 @@ class RoomListPresenterTest { val syncService = FakeSyncService(MutableStateFlow(SyncState.Running)) val presenter = createRoomListPresenter( client = FakeMatrixClient(roomListService = roomListService, encryptionService = encryptionService, syncService = syncService), - coroutineScope = scope, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -230,7 +214,6 @@ class RoomListPresenterTest { assertThat(eventWithContentAsRooms.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) eventSink(RoomListEvents.DismissRequestVerificationPrompt) assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) - scope.cancel() } } @@ -250,10 +233,8 @@ class RoomListPresenterTest { }, syncService = FakeSyncService(MutableStateFlow(SyncState.Running)), ) - val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter( client = matrixClient, - coroutineScope = scope, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -279,18 +260,16 @@ class RoomListPresenterTest { nextState.eventSink(RoomListEvents.DismissBanner) val finalState = awaitItem() assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) - scope.cancel() } } @Test fun `present - show context menu`() = runTest { - val scope = CoroutineScope(coroutineContext + SupervisorJob()) val room = FakeMatrixRoom() val client = FakeMatrixClient().apply { givenGetRoomResult(A_ROOM_ID, room) } - val presenter = createRoomListPresenter(client = client, coroutineScope = scope) + val presenter = createRoomListPresenter(client = client) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -328,18 +307,16 @@ class RoomListPresenterTest { ) ) } - scope.cancel() } } @Test fun `present - hide context menu`() = runTest { - val scope = CoroutineScope(coroutineContext + SupervisorJob()) val room = FakeMatrixRoom() val client = FakeMatrixClient().apply { givenGetRoomResult(A_ROOM_ID, room) } - val presenter = createRoomListPresenter(client = client, coroutineScope = scope) + val presenter = createRoomListPresenter(client = client) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -364,23 +341,22 @@ class RoomListPresenterTest { val hiddenState = awaitItem() assertThat(hiddenState.contextMenu).isEqualTo(RoomListState.ContextMenu.Hidden) - scope.cancel() } } @Test fun `present - leave room calls into leave room presenter`() = runTest { - val leaveRoomPresenter = FakeLeaveRoomPresenter() - val scope = CoroutineScope(coroutineContext + SupervisorJob()) - val presenter = createRoomListPresenter(leaveRoomPresenter = leaveRoomPresenter, coroutineScope = scope) + val leaveRoomEventsRecorder = EventsRecorder() + val presenter = createRoomListPresenter( + leaveRoomState = aLeaveRoomState(eventSink = leaveRoomEventsRecorder), + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID)) - assertThat(leaveRoomPresenter.events).containsExactly(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) + leaveRoomEventsRecorder.assertSingle(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) cancelAndIgnoreRemainingEvents() - scope.cancel() } } @@ -392,9 +368,7 @@ class RoomListPresenterTest { eventSink = eventRecorder ) } - val scope = CoroutineScope(coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter( - coroutineScope = scope, searchPresenter = searchPresenter, ) moleculeFlow(RecompositionMode.Immediate) { @@ -413,7 +387,6 @@ class RoomListPresenterTest { RoomListSearchEvents.ToggleSearchVisibility ) ) - scope.cancel() } } @@ -428,8 +401,7 @@ class RoomListPresenterTest { roomListService = roomListService, notificationSettingsService = notificationSettingsService ) - val scope = CoroutineScope(coroutineContext + SupervisorJob()) - val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) + val presenter = createRoomListPresenter(client = matrixClient) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -443,13 +415,11 @@ class RoomListPresenterTest { val room = updatedState.contentAsRooms().summaries.find { it.id == A_ROOM_ID.value } assertThat(room?.userDefinedNotificationMode).isEqualTo(userDefinedMode) cancelAndIgnoreRemainingEvents() - scope.cancel() } } @Test fun `present - when set is favorite event is emitted, then the action is called`() = runTest { - val scope = CoroutineScope(coroutineContext + SupervisorJob()) val setIsFavoriteResult = lambdaRecorder { _: Boolean -> Result.success(Unit) } val room = FakeMatrixRoom( setIsFavoriteResult = setIsFavoriteResult @@ -458,7 +428,7 @@ class RoomListPresenterTest { val client = FakeMatrixClient().apply { givenGetRoomResult(A_ROOM_ID, room) } - val presenter = createRoomListPresenter(client = client, coroutineScope = scope, analyticsService = analyticsService) + val presenter = createRoomListPresenter(client = client, analyticsService = analyticsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -476,13 +446,11 @@ class RoomListPresenterTest { Interaction(name = Interaction.Name.MobileRoomListRoomContextMenuFavouriteToggle) ) cancelAndIgnoreRemainingEvents() - scope.cancel() } } @Test fun `present - when room service returns no room, then contentState is Empty`() = runTest { - val scope = CoroutineScope(coroutineContext + SupervisorJob()) val roomListService = FakeRoomListService() roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(0)) val matrixClient = FakeMatrixClient( @@ -490,13 +458,11 @@ class RoomListPresenterTest { ) val presenter = createRoomListPresenter( client = matrixClient, - coroutineScope = scope, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java) - scope.cancel() } } @@ -513,14 +479,12 @@ class RoomListPresenterTest { givenGetRoomResult(A_ROOM_ID_3, room3) } val analyticsService = FakeAnalyticsService() - val scope = CoroutineScope(coroutineContext + SupervisorJob()) val clearMessagesForRoomLambda = lambdaRecorder { _, _ -> } val notificationCleaner = FakeNotificationCleaner( clearMessagesForRoomLambda = clearMessagesForRoomLambda, ) val presenter = createRoomListPresenter( client = matrixClient, - coroutineScope = scope, sessionPreferencesStore = sessionPreferencesStore, analyticsService = analyticsService, notificationCleaner = notificationCleaner, @@ -557,7 +521,6 @@ class RoomListPresenterTest { Interaction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle), ) cancelAndIgnoreRemainingEvents() - scope.cancel() } } @@ -568,7 +531,6 @@ class RoomListPresenterTest { anAcceptDeclineInviteState(eventSink = eventSinkRecorder) } val roomListService = FakeRoomListService() - val scope = CoroutineScope(coroutineContext + SupervisorJob()) val matrixClient = FakeMatrixClient( roomListService = roomListService, ) @@ -578,7 +540,6 @@ class RoomListPresenterTest { roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomListService.postAllRooms(listOf(roomSummary)) val presenter = createRoomListPresenter( - coroutineScope = scope, client = matrixClient, acceptDeclineInvitePresenter = acceptDeclinePresenter ) @@ -609,7 +570,6 @@ class RoomListPresenterTest { fun `present - UpdateVisibleRange will cancel the previous subscription if called too soon`() = runTest { val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List -> } val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda) - val scope = CoroutineScope(coroutineContext + SupervisorJob()) val matrixClient = FakeMatrixClient( roomListService = roomListService, ) @@ -619,7 +579,6 @@ class RoomListPresenterTest { roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomListService.postAllRooms(listOf(roomSummary)) val presenter = createRoomListPresenter( - coroutineScope = scope, client = matrixClient, ) presenter.test { @@ -640,7 +599,6 @@ class RoomListPresenterTest { fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest { val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List -> } val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda) - val scope = CoroutineScope(coroutineContext + SupervisorJob()) val matrixClient = FakeMatrixClient( roomListService = roomListService, ) @@ -650,7 +608,6 @@ class RoomListPresenterTest { roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomListService.postAllRooms(listOf(roomSummary)) val presenter = createRoomListPresenter( - coroutineScope = scope, client = matrixClient, ) presenter.test { @@ -673,28 +630,23 @@ class RoomListPresenterTest { client: MatrixClient = FakeMatrixClient(), networkMonitor: NetworkMonitor = FakeNetworkMonitor(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), - leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(), + leaveRoomState: LeaveRoomState = aLeaveRoomState(), lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply { givenFormat(A_FORMATTED_DATE) }, roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(), sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(), - coroutineScope: CoroutineScope, analyticsService: AnalyticsService = FakeAnalyticsService(), filtersPresenter: Presenter = Presenter { aRoomListFiltersState() }, searchPresenter: Presenter = Presenter { aRoomListSearchState() }, acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), - logoutPresenter: DirectLogoutPresenter = object : DirectLogoutPresenter { - @Composable - override fun present() = aDirectLogoutState() - }, ) = RoomListPresenter( client = client, networkMonitor = networkMonitor, snackbarDispatcher = snackbarDispatcher, - leaveRoomPresenter = leaveRoomPresenter, + leaveRoomPresenter = { leaveRoomState }, roomListDataSource = RoomListDataSource( roomListService = client.roomListService, roomListRoomSummaryFactory = RoomListRoomSummaryFactory( @@ -703,7 +655,7 @@ class RoomListPresenterTest { ), coroutineDispatchers = testCoroutineDispatchers(), notificationSettingsService = client.notificationSettingsService(), - appScope = coroutineScope + appScope = backgroundScope ), featureFlagService = featureFlagService, indicatorService = DefaultIndicatorService( @@ -715,8 +667,8 @@ class RoomListPresenterTest { filtersPresenter = filtersPresenter, analyticsService = analyticsService, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, - fullScreenIntentPermissionsPresenter = FakeFullScreenIntentPermissionsPresenter(), + fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() }, notificationCleaner = notificationCleaner, - logoutPresenter = logoutPresenter, + logoutPresenter = { aDirectLogoutState() }, ) } diff --git a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsPresenter.kt b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsPresenter.kt deleted file mode 100644 index 4e56a4f938..0000000000 --- a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsPresenter.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.libraries.fullscreenintent.api - -import io.element.android.libraries.architecture.Presenter - -interface FullScreenIntentPermissionsPresenter : Presenter diff --git a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt new file mode 100644 index 0000000000..d4e9417128 --- /dev/null +++ b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.fullscreenintent.api + +fun aFullScreenIntentPermissionsState( + permissionGranted: Boolean = true, + shouldDisplay: Boolean = false, + openFullScreenIntentSettings: () -> Unit = {}, + dismissFullScreenIntentBanner: () -> Unit = {}, +) = FullScreenIntentPermissionsState( + permissionGranted = permissionGranted, + shouldDisplayBanner = shouldDisplay, + openFullScreenIntentSettings = openFullScreenIntentSettings, + dismissFullScreenIntentBanner = dismissFullScreenIntentBanner, +) diff --git a/libraries/fullscreenintent/impl/build.gradle.kts b/libraries/fullscreenintent/impl/build.gradle.kts index a467e11fb2..cd32e316b0 100644 --- a/libraries/fullscreenintent/impl/build.gradle.kts +++ b/libraries/fullscreenintent/impl/build.gradle.kts @@ -27,7 +27,6 @@ dependencies { implementation(projects.services.toolbox.api) implementation(libs.androidx.datastore.preferences) - testImplementation(projects.libraries.fullscreenintent.test) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) diff --git a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/DefaultFullScreenIntentPermissionsPresenter.kt b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt similarity index 92% rename from libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/DefaultFullScreenIntentPermissionsPresenter.kt rename to libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt index 939ecd474a..bc1dd24956 100644 --- a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/DefaultFullScreenIntentPermissionsPresenter.kt +++ b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt @@ -19,11 +19,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.core.app.NotificationManagerCompat import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit -import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsPresenter import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher @@ -33,14 +32,13 @@ import kotlinx.coroutines.launch import javax.inject.Inject @SingleIn(AppScope::class) -@ContributesBinding(AppScope::class) -class DefaultFullScreenIntentPermissionsPresenter @Inject constructor( +class FullScreenIntentPermissionsPresenter @Inject constructor( private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, private val externalIntentLauncher: ExternalIntentLauncher, private val buildMeta: BuildMeta, private val notificationManagerCompat: NotificationManagerCompat, preferencesDataStoreFactory: PreferenceDataStoreFactory, -) : FullScreenIntentPermissionsPresenter { +) : Presenter { companion object { private const val PREF_KEY_FULL_SCREEN_INTENT_BANNER_DISMISSED = "PREF_KEY_FULL_SCREEN_INTENT_BANNER_DISMISSED" } diff --git a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/di/FullScreenIntentModule.kt b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/di/FullScreenIntentModule.kt new file mode 100644 index 0000000000..2dca4dbbc7 --- /dev/null +++ b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/di/FullScreenIntentModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.fullscreenintent.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState +import io.element.android.libraries.fullscreenintent.impl.FullScreenIntentPermissionsPresenter + +@ContributesTo(AppScope::class) +@Module +interface FullScreenIntentModule { + @Binds + fun bindFullScreenIntentPermissionsPresenter(presenter: FullScreenIntentPermissionsPresenter): Presenter +} diff --git a/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/DefaultFullScreenIntentPermissionsPresenterTest.kt b/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt similarity index 96% rename from libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/DefaultFullScreenIntentPermissionsPresenterTest.kt rename to libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt index 9de5287f42..415b53befd 100644 --- a/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/DefaultFullScreenIntentPermissionsPresenterTest.kt +++ b/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt @@ -15,7 +15,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.fullscreenintent.impl.DefaultFullScreenIntentPermissionsPresenter +import io.element.android.libraries.fullscreenintent.impl.FullScreenIntentPermissionsPresenter import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher @@ -32,7 +32,7 @@ import org.junit.Rule import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) -class DefaultFullScreenIntentPermissionsPresenterTest { +class FullScreenIntentPermissionsPresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -129,7 +129,7 @@ class DefaultFullScreenIntentPermissionsPresenterTest { externalIntentLauncher: ExternalIntentLauncher = FakeExternalIntentLauncher(), buildMeta: BuildMeta = aBuildMeta(), notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true) - ) = DefaultFullScreenIntentPermissionsPresenter( + ) = FullScreenIntentPermissionsPresenter( buildVersionSdkIntProvider = buildVersionSdkIntProvider, externalIntentLauncher = externalIntentLauncher, buildMeta = buildMeta, diff --git a/libraries/fullscreenintent/test/build.gradle.kts b/libraries/fullscreenintent/test/build.gradle.kts deleted file mode 100644 index 01712589e3..0000000000 --- a/libraries/fullscreenintent/test/build.gradle.kts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -plugins { - id("io.element.android-compose-library") -} - -android { - namespace = "io.element.android.libraries.fullscreenintent.test" -} - -dependencies { - api(projects.libraries.fullscreenintent.api) - implementation(projects.libraries.architecture) -} diff --git a/libraries/fullscreenintent/test/src/main/kotlin/io/element/android/libraries/fullscreenintent/test/FakeFullScreenIntentPermissionsPresenter.kt b/libraries/fullscreenintent/test/src/main/kotlin/io/element/android/libraries/fullscreenintent/test/FakeFullScreenIntentPermissionsPresenter.kt deleted file mode 100644 index f1613b9fbb..0000000000 --- a/libraries/fullscreenintent/test/src/main/kotlin/io/element/android/libraries/fullscreenintent/test/FakeFullScreenIntentPermissionsPresenter.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.libraries.fullscreenintent.test - -import androidx.compose.runtime.Composable -import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsPresenter -import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState - -class FakeFullScreenIntentPermissionsPresenter : FullScreenIntentPermissionsPresenter { - var state = FullScreenIntentPermissionsState( - permissionGranted = true, - shouldDisplayBanner = false, - dismissFullScreenIntentBanner = {}, - openFullScreenIntentSettings = {}, - ) - @Composable - override fun present(): FullScreenIntentPermissionsState { - return state - } -} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt index 41b4a5a340..e06540f7a4 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt @@ -24,10 +24,7 @@ import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolde import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import io.element.android.tests.testutils.lambda.lambdaRecorder import io.mockk.mockk -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -39,7 +36,6 @@ class DefaultOnMissedCallNotificationHandlerTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `addMissedCallNotification - should add missed call notification`() = runTest { - val childScope = CoroutineScope(coroutineContext + SupervisorJob()) val dataFactory = FakeNotificationDataFactory( messageEventToNotificationsResult = lambdaRecorder { _, _, _ -> emptyList() } ) @@ -59,7 +55,7 @@ class DefaultOnMissedCallNotificationHandlerTest { notificationDataFactory = dataFactory, ), appNavigationStateService = FakeAppNavigationStateService(), - coroutineScope = childScope, + coroutineScope = backgroundScope, matrixClientProvider = FakeMatrixClientProvider(), imageLoaderHolder = FakeImageLoaderHolder(), activeNotificationsProvider = FakeActiveNotificationsProvider(), @@ -76,8 +72,5 @@ class DefaultOnMissedCallNotificationHandlerTest { runCurrent() dataFactory.messageEventToNotificationsResult.assertions().isCalledOnce() - - // Cancel the coroutine scope so the test can finish - childScope.cancel() } } 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 c6a2d49c89..59969ec622 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 @@ -13,8 +13,8 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter import io.element.android.features.invite.impl.response.AcceptDeclineInviteView -import io.element.android.features.leaveroom.impl.DefaultLeaveRoomPresenter -import io.element.android.features.logout.impl.direct.DefaultDirectLogoutPresenter +import io.element.android.features.leaveroom.impl.LeaveRoomPresenter +import io.element.android.features.logout.impl.direct.DirectLogoutPresenter import io.element.android.features.networkmonitor.impl.DefaultNetworkMonitor import io.element.android.features.roomlist.impl.RoomListPresenter import io.element.android.features.roomlist.impl.RoomListView @@ -33,8 +33,7 @@ import io.element.android.libraries.eventformatter.impl.DefaultRoomLastMessageFo 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.fullscreenintent.api.FullScreenIntentPermissionsPresenter -import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState +import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.indicator.impl.DefaultIndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -87,7 +86,7 @@ class RoomListScreen( client = matrixClient, networkMonitor = DefaultNetworkMonitor(context, Singleton.appScope), snackbarDispatcher = SnackbarDispatcher(), - leaveRoomPresenter = DefaultLeaveRoomPresenter(matrixClient, RoomMembershipObserver(), coroutineDispatchers), + leaveRoomPresenter = LeaveRoomPresenter(matrixClient, RoomMembershipObserver(), coroutineDispatchers), roomListDataSource = RoomListDataSource( roomListService = matrixClient.roomListService, roomListRoomSummaryFactory = roomListRoomSummaryFactory, @@ -123,19 +122,9 @@ class RoomListScreen( notificationCleaner = FakeNotificationCleaner(), ), analyticsService = NoopAnalyticsService(), - fullScreenIntentPermissionsPresenter = object : FullScreenIntentPermissionsPresenter { - @Composable - override fun present(): FullScreenIntentPermissionsState { - return FullScreenIntentPermissionsState( - permissionGranted = true, - shouldDisplayBanner = false, - dismissFullScreenIntentBanner = {}, - openFullScreenIntentSettings = {} - ) - } - }, + fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() }, notificationCleaner = FakeNotificationCleaner(), - logoutPresenter = DefaultDirectLogoutPresenter(matrixClient, encryptionService), + logoutPresenter = DirectLogoutPresenter(matrixClient, encryptionService), ) @Composable diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPresenterTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPresenterTest.kt new file mode 100644 index 0000000000..5e949764b0 --- /dev/null +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPresenterTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023, 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.tests.konsist + +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.ext.list.constructors +import com.lemonappdev.konsist.api.ext.list.withAllParentsOf +import com.lemonappdev.konsist.api.verify.assertTrue +import io.element.android.libraries.architecture.Presenter +import org.junit.Test + +class KonsistPresenterTest { + @Test + fun `'Presenter' should not depend on other presenters`() { + Konsist.scopeFromProject() + .classes() + .withAllParentsOf(Presenter::class) + .constructors + .assertTrue { constructor -> + val result = constructor.parameters.none { parameter -> + parameter.type.name.endsWith("Presenter") + } + result + } + } +} diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistTestTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistTestTest.kt index afaef2f54b..4ae380a189 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistTestTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistTestTest.kt @@ -7,16 +7,32 @@ package io.element.android.tests.konsist +import com.google.common.truth.Truth.assertThat import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutOverrideModifier +import com.lemonappdev.konsist.api.ext.list.withAnnotationOf import com.lemonappdev.konsist.api.ext.list.withFunction import com.lemonappdev.konsist.api.ext.list.withReturnType +import com.lemonappdev.konsist.api.ext.list.withoutAnnotationOf import com.lemonappdev.konsist.api.ext.list.withoutName import com.lemonappdev.konsist.api.verify.assertFalse import com.lemonappdev.konsist.api.verify.assertTrue +import org.junit.Ignore import org.junit.Test class KonsistTestTest { + @Test + fun `Ensure that unit tests are detected`() { + val numberOfTests = Konsist + .scopeFromTest() + .functions() + .withAnnotationOf(Test::class) + .withoutAnnotationOf(Ignore::class) + .size + println("Number of unit tests: $numberOfTests") + assertThat(numberOfTests).isGreaterThan(2000) + } + @Test fun `Classes name containing @Test must end with 'Test'`() { Konsist