Merge pull request #3618 from element-hq/feature/bma/injectPresenter

Ensure that `Presenter`s do not depend on other presenters.
This commit is contained in:
Benoit Marty 2024-10-07 19:22:59 +02:00 committed by GitHub
commit c7c870b5b4
73 changed files with 849 additions and 1029 deletions

View file

@ -12,8 +12,8 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.analytics.plan.SuperProperties
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.share.api.ShareService import io.element.android.features.share.api.ShareService
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.SdkMetadata import io.element.android.libraries.matrix.api.SdkMetadata
@ -22,8 +22,8 @@ import io.element.android.services.apperror.api.AppErrorStateService
import javax.inject.Inject import javax.inject.Inject
class RootPresenter @Inject constructor( class RootPresenter @Inject constructor(
private val crashDetectionPresenter: CrashDetectionPresenter, private val crashDetectionPresenter: Presenter<CrashDetectionState>,
private val rageshakeDetectionPresenter: RageshakeDetectionPresenter, private val rageshakeDetectionPresenter: Presenter<RageshakeDetectionState>,
private val appErrorStateService: AppErrorStateService, private val appErrorStateService: AppErrorStateService,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val shareService: ShareService, private val shareService: ShareService,

View file

@ -12,17 +12,11 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootPresenter
import io.element.android.features.rageshake.impl.crash.DefaultCrashDetectionPresenter import io.element.android.features.rageshake.api.crash.aCrashDetectionState
import io.element.android.features.rageshake.impl.detection.DefaultRageshakeDetectionPresenter import io.element.android.features.rageshake.api.detection.aRageshakeDetectionState
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.share.api.ShareService import io.element.android.features.share.api.ShareService
import io.element.android.features.share.test.FakeShareService import io.element.android.features.share.test.FakeShareService
import io.element.android.libraries.matrix.test.FakeSdkMetadata 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.analytics.test.FakeAnalyticsService
import io.element.android.services.apperror.api.AppErrorState import io.element.android.services.apperror.api.AppErrorState
import io.element.android.services.apperror.api.AppErrorStateService import io.element.android.services.apperror.api.AppErrorStateService
@ -44,7 +38,6 @@ class RootPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.crashDetectionState.crashDetected).isFalse() assertThat(initialState.crashDetectionState.crashDetected).isFalse()
} }
@ -61,7 +54,7 @@ class RootPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(2) skipItems(1)
lambda.assertions().isCalledOnce() lambda.assertions().isCalledOnce()
} }
} }
@ -76,8 +69,6 @@ class RootPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.errorState).isInstanceOf(AppErrorState.Error::class.java) assertThat(initialState.errorState).isInstanceOf(AppErrorState.Error::class.java)
val initialErrorState = initialState.errorState as AppErrorState.Error val initialErrorState = initialState.errorState as AppErrorState.Error
@ -93,25 +84,9 @@ class RootPresenterTest {
appErrorService: AppErrorStateService = DefaultAppErrorStateService(), appErrorService: AppErrorStateService = DefaultAppErrorStateService(),
shareService: ShareService = FakeShareService {}, shareService: ShareService = FakeShareService {},
): RootPresenter { ): 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( return RootPresenter(
crashDetectionPresenter = crashDetectionPresenter, crashDetectionPresenter = { aCrashDetectionState() },
rageshakeDetectionPresenter = rageshakeDetectionPresenter, rageshakeDetectionPresenter = { aRageshakeDetectionState() },
appErrorStateService = appErrorService, appErrorStateService = appErrorService,
analyticsService = FakeAnalyticsService(), analyticsService = FakeAnalyticsService(),
shareService = shareService, shareService = shareService,

View file

@ -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<AnalyticsPreferencesState>

View file

@ -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<AnalyticsPreferencesState>
}

View file

@ -10,23 +10,20 @@ package io.element.android.features.analytics.impl.preferences
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.AnalyticsConfig import io.element.android.appconfig.AnalyticsConfig
import io.element.android.features.analytics.api.AnalyticsOptInEvents 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.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.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(AppScope::class) class AnalyticsPreferencesPresenter @Inject constructor(
class DefaultAnalyticsPreferencesPresenter @Inject constructor(
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) : AnalyticsPreferencesPresenter { ) : Presenter<AnalyticsPreferencesState> {
@Composable @Composable
override fun present(): AnalyticsPreferencesState { override fun present(): AnalyticsPreferencesState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()

View file

@ -25,7 +25,7 @@ class AnalyticsPreferencesPresenterTest {
@Test @Test
fun `present - initial state available`() = runTest { fun `present - initial state available`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter( val presenter = AnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true), FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
aBuildMeta() aBuildMeta()
) )
@ -41,7 +41,7 @@ class AnalyticsPreferencesPresenterTest {
@Test @Test
fun `present - initial state not available`() = runTest { fun `present - initial state not available`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter( val presenter = AnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = false, didAskUserConsent = false), FakeAnalyticsService(isEnabled = false, didAskUserConsent = false),
aBuildMeta() aBuildMeta()
) )
@ -55,7 +55,7 @@ class AnalyticsPreferencesPresenterTest {
@Test @Test
fun `present - enable and disable`() = runTest { fun `present - enable and disable`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter( val presenter = AnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true), FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
aBuildMeta() aBuildMeta()
) )

View file

@ -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.ftue.impl.state.FtueStep
import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.test.FakeLockScreenService 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.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService 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.permissions.impl.FakePermissionStateProvider
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -36,8 +37,9 @@ class DefaultFtueServiceTest {
val sessionVerificationService = FakeSessionVerificationService().apply { val sessionVerificationService = FakeSessionVerificationService().apply {
givenVerifiedStatus(SessionVerifiedStatus.Unknown) givenVerifiedStatus(SessionVerifiedStatus.Unknown)
} }
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val service = createDefaultFtueService(
val service = createDefaultFtueService(coroutineScope, sessionVerificationService) sessionVerificationService = sessionVerificationService,
)
service.state.test { service.state.test {
// Verification state is unknown, we don't display the flow yet // Verification state is unknown, we don't display the flow yet
@ -47,9 +49,6 @@ class DefaultFtueServiceTest {
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified) sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
assertThat(awaitItem()).isEqualTo(FtueState.Incomplete) assertThat(awaitItem()).isEqualTo(FtueState.Incomplete)
} }
// Cleanup
coroutineScope.cancel()
} }
@Test @Test
@ -58,10 +57,7 @@ class DefaultFtueServiceTest {
val sessionVerificationService = FakeSessionVerificationService() val sessionVerificationService = FakeSessionVerificationService()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true) val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true)
val lockScreenService = FakeLockScreenService() val lockScreenService = FakeLockScreenService()
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val service = createDefaultFtueService( val service = createDefaultFtueService(
coroutineScope = coroutineScope,
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
analyticsService = analyticsService, analyticsService = analyticsService,
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
@ -75,9 +71,6 @@ class DefaultFtueServiceTest {
service.updateState() service.updateState()
assertThat(service.state.value).isEqualTo(FtueState.Complete) assertThat(service.state.value).isEqualTo(FtueState.Complete)
// Cleanup
coroutineScope.cancel()
} }
@Test @Test
@ -88,10 +81,7 @@ class DefaultFtueServiceTest {
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
val lockScreenService = FakeLockScreenService() val lockScreenService = FakeLockScreenService()
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val service = createDefaultFtueService( val service = createDefaultFtueService(
coroutineScope = coroutineScope,
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
analyticsService = analyticsService, analyticsService = analyticsService,
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
@ -126,20 +116,15 @@ class DefaultFtueServiceTest {
// Final state // Final state
null, null,
) )
// Cleanup
coroutineScope.cancel()
} }
@Test @Test
fun `if a check for a step is true, start from the next one`() = runTest { fun `if a check for a step is true, start from the next one`() = runTest {
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val sessionVerificationService = FakeSessionVerificationService() val sessionVerificationService = FakeSessionVerificationService()
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
val lockScreenService = FakeLockScreenService() val lockScreenService = FakeLockScreenService()
val service = createDefaultFtueService( val service = createDefaultFtueService(
coroutineScope = coroutineScope,
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
analyticsService = analyticsService, analyticsService = analyticsService,
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
@ -155,14 +140,10 @@ class DefaultFtueServiceTest {
analyticsService.setDidAskUserConsent() analyticsService.setDidAskUserConsent()
assertThat(service.getNextStep(null)).isNull() assertThat(service.getNextStep(null)).isNull()
// Cleanup
coroutineScope.cancel()
} }
@Test @Test
fun `if version is older than 13 we don't display the notification opt in screen`() = runTest { 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 sessionVerificationService = FakeSessionVerificationService()
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val lockScreenService = FakeLockScreenService() val lockScreenService = FakeLockScreenService()
@ -170,7 +151,6 @@ class DefaultFtueServiceTest {
val service = createDefaultFtueService( val service = createDefaultFtueService(
sdkIntVersion = Build.VERSION_CODES.M, sdkIntVersion = Build.VERSION_CODES.M,
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
coroutineScope = coroutineScope,
analyticsService = analyticsService, analyticsService = analyticsService,
lockScreenService = lockScreenService, lockScreenService = lockScreenService,
) )
@ -182,14 +162,10 @@ class DefaultFtueServiceTest {
analyticsService.setDidAskUserConsent() analyticsService.setDidAskUserConsent()
assertThat(service.getNextStep(null)).isNull() assertThat(service.getNextStep(null)).isNull()
// Cleanup
coroutineScope.cancel()
} }
@Test @Test
fun `reset do the expected actions S`() = runTest { fun `reset do the expected actions S`() = runTest {
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val resetAnalyticsLambda = lambdaRecorder<Unit> { } val resetAnalyticsLambda = lambdaRecorder<Unit> { }
val analyticsService = FakeAnalyticsService( val analyticsService = FakeAnalyticsService(
resetLambda = resetAnalyticsLambda resetLambda = resetAnalyticsLambda
@ -199,7 +175,6 @@ class DefaultFtueServiceTest {
resetPermissionLambda = resetPermissionLambda resetPermissionLambda = resetPermissionLambda
) )
val service = createDefaultFtueService( val service = createDefaultFtueService(
coroutineScope = coroutineScope,
sdkIntVersion = Build.VERSION_CODES.S, sdkIntVersion = Build.VERSION_CODES.S,
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
analyticsService = analyticsService, analyticsService = analyticsService,
@ -211,7 +186,6 @@ class DefaultFtueServiceTest {
@Test @Test
fun `reset do the expected actions TIRAMISU`() = runTest { fun `reset do the expected actions TIRAMISU`() = runTest {
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val resetLambda = lambdaRecorder<Unit> { } val resetLambda = lambdaRecorder<Unit> { }
val analyticsService = FakeAnalyticsService( val analyticsService = FakeAnalyticsService(
resetLambda = resetLambda resetLambda = resetLambda
@ -221,7 +195,6 @@ class DefaultFtueServiceTest {
resetPermissionLambda = resetPermissionLambda resetPermissionLambda = resetPermissionLambda
) )
val service = createDefaultFtueService( val service = createDefaultFtueService(
coroutineScope = coroutineScope,
sdkIntVersion = Build.VERSION_CODES.TIRAMISU, sdkIntVersion = Build.VERSION_CODES.TIRAMISU,
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
analyticsService = analyticsService, analyticsService = analyticsService,
@ -232,17 +205,16 @@ class DefaultFtueServiceTest {
.with(value("android.permission.POST_NOTIFICATIONS")) .with(value("android.permission.POST_NOTIFICATIONS"))
} }
private fun createDefaultFtueService( private fun TestScope.createDefaultFtueService(
coroutineScope: CoroutineScope, sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
analyticsService: AnalyticsService = FakeAnalyticsService(), analyticsService: AnalyticsService = FakeAnalyticsService(),
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false), permissionStateProvider: PermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
lockScreenService: LockScreenService = FakeLockScreenService(), lockScreenService: LockScreenService = FakeLockScreenService(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
// First version where notification permission is required // First version where notification permission is required
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU, sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
) = DefaultFtueService( ) = DefaultFtueService(
sessionCoroutineScope = coroutineScope, sessionCoroutineScope = backgroundScope,
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion), sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
analyticsService = analyticsService, analyticsService = analyticsService,

View file

@ -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<LeaveRoomState> {
@Composable
override fun present(): LeaveRoomState
}

View file

@ -57,9 +57,10 @@ fun aLeaveRoomState(
confirmation: LeaveRoomState.Confirmation = LeaveRoomState.Confirmation.Hidden, confirmation: LeaveRoomState.Confirmation = LeaveRoomState.Confirmation.Hidden,
progress: LeaveRoomState.Progress = LeaveRoomState.Progress.Hidden, progress: LeaveRoomState.Progress = LeaveRoomState.Progress.Hidden,
error: LeaveRoomState.Error = LeaveRoomState.Error.Hidden, error: LeaveRoomState.Error = LeaveRoomState.Error.Hidden,
eventSink: (LeaveRoomEvent) -> Unit = {},
) = LeaveRoomState( ) = LeaveRoomState(
confirmation = confirmation, confirmation = confirmation,
progress = progress, progress = progress,
error = error, error = error,
eventSink = {}, eventSink = eventSink,
) )

View file

@ -12,16 +12,14 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope 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.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
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Dm 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.Generic
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.LastUserInRoom import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.LastUserInRoom
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.PrivateRoom 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.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.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
@ -30,12 +28,11 @@ import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(SessionScope::class) class LeaveRoomPresenter @Inject constructor(
class DefaultLeaveRoomPresenter @Inject constructor(
private val client: MatrixClient, private val client: MatrixClient,
private val roomMembershipObserver: RoomMembershipObserver, private val roomMembershipObserver: RoomMembershipObserver,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
) : LeaveRoomPresenter { ) : Presenter<LeaveRoomState> {
@Composable @Composable
override fun present(): LeaveRoomState { override fun present(): LeaveRoomState {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()

View file

@ -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<LeaveRoomState>
}

View file

@ -27,13 +27,13 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
class DefaultLeaveRoomPresenterTest { class LeaveRoomPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@Test @Test
fun `present - initial state hides all dialogs`() = runTest { fun `present - initial state hides all dialogs`() = runTest {
val presenter = createDefaultLeaveRoomPresenter() val presenter = createLeaveRoomPresenter()
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -46,7 +46,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - show generic confirmation`() = runTest { fun `present - show generic confirmation`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -66,7 +66,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - show private room confirmation`() = runTest { fun `present - show private room confirmation`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -86,7 +86,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - show last user in room confirmation`() = runTest { fun `present - show last user in room confirmation`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -106,7 +106,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - show DM confirmation`() = runTest { fun `present - show DM confirmation`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -127,7 +127,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - leaving a room leaves the room`() = runTest { fun `present - leaving a room leaves the room`() = runTest {
val roomMembershipObserver = RoomMembershipObserver() val roomMembershipObserver = RoomMembershipObserver()
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -151,7 +151,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - show error if leave room fails`() = runTest { fun `present - show error if leave room fails`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -175,7 +175,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - show progress indicator while leaving a room`() = runTest { fun `present - show progress indicator while leaving a room`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -199,7 +199,7 @@ class DefaultLeaveRoomPresenterTest {
@Test @Test
fun `present - hide error hides the error`() = runTest { fun `present - hide error hides the error`() = runTest {
val presenter = createDefaultLeaveRoomPresenter( val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply { client = FakeMatrixClient().apply {
givenGetRoomResult( givenGetRoomResult(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -225,10 +225,10 @@ class DefaultLeaveRoomPresenterTest {
} }
} }
private fun TestScope.createDefaultLeaveRoomPresenter( private fun TestScope.createLeaveRoomPresenter(
client: MatrixClient = FakeMatrixClient(), client: MatrixClient = FakeMatrixClient(),
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
): DefaultLeaveRoomPresenter = DefaultLeaveRoomPresenter( ): LeaveRoomPresenter = LeaveRoomPresenter(
client = client, client = client,
roomMembershipObserver = roomMembershipObserver, roomMembershipObserver = roomMembershipObserver,
dispatchers = testCoroutineDispatchers(false), dispatchers = testCoroutineDispatchers(false),

View file

@ -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)
}

View file

@ -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<LeaveRoomEvent>()
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
}

View file

@ -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<ChangeServerState>
}

View file

@ -10,12 +10,12 @@ package io.element.android.features.login.impl.screens.changeaccountprovider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import io.element.android.appconfig.AuthenticationConfig import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.login.impl.accountprovider.AccountProvider 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 io.element.android.libraries.architecture.Presenter
import javax.inject.Inject import javax.inject.Inject
class ChangeAccountProviderPresenter @Inject constructor( class ChangeAccountProviderPresenter @Inject constructor(
private val changeServerPresenter: ChangeServerPresenter, private val changeServerPresenter: Presenter<ChangeServerState>,
) : Presenter<ChangeAccountProviderState> { ) : Presenter<ChangeAccountProviderState> {
@Composable @Composable
override fun present(): ChangeAccountProviderState { override fun present(): ChangeAccountProviderState {

View file

@ -15,7 +15,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue 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.HomeserverData
import io.element.android.features.login.impl.resolver.HomeserverResolver import io.element.android.features.login.impl.resolver.HomeserverResolver
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -27,7 +27,7 @@ import javax.inject.Inject
class SearchAccountProviderPresenter @Inject constructor( class SearchAccountProviderPresenter @Inject constructor(
private val homeserverResolver: HomeserverResolver, private val homeserverResolver: HomeserverResolver,
private val changeServerPresenter: ChangeServerPresenter, private val changeServerPresenter: Presenter<ChangeServerState>,
) : Presenter<SearchAccountProviderState> { ) : Presenter<SearchAccountProviderState> {
@Composable @Composable
override fun present(): SearchAccountProviderState { override fun present(): SearchAccountProviderState {

View file

@ -12,9 +12,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.accountprovider.AccountProvider 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.aChangeServerState
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@ -26,12 +24,8 @@ class ChangeAccountProviderPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val changeServerPresenter = ChangeServerPresenter(
FakeMatrixAuthenticationService(),
AccountProviderDataSource()
)
val presenter = ChangeAccountProviderPresenter( val presenter = ChangeAccountProviderPresenter(
changeServerPresenter changeServerPresenter = { aChangeServerState() }
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()

View file

@ -11,15 +11,13 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.changeserver.aChangeServerState
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.features.login.impl.resolver.HomeserverResolver 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.FakeWellknownRequest
import io.element.android.features.login.impl.resolver.network.WellKnown import io.element.android.features.login.impl.resolver.network.WellKnown
import io.element.android.features.login.impl.resolver.network.WellKnownBaseConfig import io.element.android.features.login.impl.resolver.network.WellKnownBaseConfig
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL 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.WarmUpRule
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -33,13 +31,9 @@ class SearchAccountProviderPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val fakeWellknownRequest = FakeWellknownRequest() val fakeWellknownRequest = FakeWellknownRequest()
val changeServerPresenter = ChangeServerPresenter(
FakeMatrixAuthenticationService(),
AccountProviderDataSource()
)
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter changeServerPresenter = { aChangeServerState() }
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -53,13 +47,9 @@ class SearchAccountProviderPresenterTest {
@Test @Test
fun `present - enter text no result`() = runTest { fun `present - enter text no result`() = runTest {
val fakeWellknownRequest = FakeWellknownRequest() val fakeWellknownRequest = FakeWellknownRequest()
val changeServerPresenter = ChangeServerPresenter(
FakeMatrixAuthenticationService(),
AccountProviderDataSource()
)
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter changeServerPresenter = { aChangeServerState() }
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -77,13 +67,9 @@ class SearchAccountProviderPresenterTest {
@Test @Test
fun `present - enter valid url no wellknown`() = runTest { fun `present - enter valid url no wellknown`() = runTest {
val fakeWellknownRequest = FakeWellknownRequest() val fakeWellknownRequest = FakeWellknownRequest()
val changeServerPresenter = ChangeServerPresenter(
FakeMatrixAuthenticationService(),
AccountProviderDataSource()
)
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter changeServerPresenter = { aChangeServerState() }
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -112,13 +98,9 @@ class SearchAccountProviderPresenterTest {
"https://test.io" to aWellKnown(), "https://test.io" to aWellKnown(),
) )
) )
val changeServerPresenter = ChangeServerPresenter(
FakeMatrixAuthenticationService(),
AccountProviderDataSource()
)
val presenter = SearchAccountProviderPresenter( val presenter = SearchAccountProviderPresenter(
HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter changeServerPresenter = { aChangeServerState() }
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()

View file

@ -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<DirectLogoutState>

View file

@ -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<DirectLogoutState>
}

View file

@ -14,14 +14,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope 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.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.api.direct.DirectLogoutState
import io.element.android.features.logout.impl.tools.isBackingUp import io.element.android.features.logout.impl.tools.isBackingUp
import io.element.android.libraries.architecture.AsyncAction 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.architecture.runCatchingUpdatingState
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient 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.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
@ -29,11 +27,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(SessionScope::class) class DirectLogoutPresenter @Inject constructor(
class DefaultDirectLogoutPresenter @Inject constructor(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val encryptionService: EncryptionService, private val encryptionService: EncryptionService,
) : DirectLogoutPresenter { ) : Presenter<DirectLogoutState> {
@Composable @Composable
override fun present(): DirectLogoutState { override fun present(): DirectLogoutState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()

View file

@ -26,13 +26,13 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
class DefaultDirectLogoutPresenterTest { class DirectLogoutPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = createDefaultDirectLogoutPresenter() val presenter = createDirectLogoutPresenter()
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -44,7 +44,7 @@ class DefaultDirectLogoutPresenterTest {
@Test @Test
fun `present - initial state - last session`() = runTest { fun `present - initial state - last session`() = runTest {
val presenter = createDefaultDirectLogoutPresenter( val presenter = createDirectLogoutPresenter(
encryptionService = FakeEncryptionService().apply { encryptionService = FakeEncryptionService().apply {
emitIsLastDevice(true) emitIsLastDevice(true)
} }
@ -66,7 +66,7 @@ class DefaultDirectLogoutPresenterTest {
emit(BackupUploadState.Waiting) emit(BackupUploadState.Waiting)
} }
) )
val presenter = createDefaultDirectLogoutPresenter( val presenter = createDirectLogoutPresenter(
encryptionService = encryptionService encryptionService = encryptionService
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@ -81,7 +81,7 @@ class DefaultDirectLogoutPresenterTest {
@Test @Test
fun `present - logout then cancel`() = runTest { fun `present - logout then cancel`() = runTest {
val presenter = createDefaultDirectLogoutPresenter() val presenter = createDirectLogoutPresenter()
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -97,7 +97,7 @@ class DefaultDirectLogoutPresenterTest {
@Test @Test
fun `present - logout then confirm`() = runTest { fun `present - logout then confirm`() = runTest {
val presenter = createDefaultDirectLogoutPresenter() val presenter = createDirectLogoutPresenter()
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -120,7 +120,7 @@ class DefaultDirectLogoutPresenterTest {
throw A_THROWABLE throw A_THROWABLE
} }
} }
val presenter = createDefaultDirectLogoutPresenter( val presenter = createDirectLogoutPresenter(
matrixClient, matrixClient,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@ -152,7 +152,7 @@ class DefaultDirectLogoutPresenterTest {
} }
} }
} }
val presenter = createDefaultDirectLogoutPresenter( val presenter = createDirectLogoutPresenter(
matrixClient, matrixClient,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@ -179,10 +179,10 @@ class DefaultDirectLogoutPresenterTest {
return awaitItem() return awaitItem()
} }
private fun createDefaultDirectLogoutPresenter( private fun createDirectLogoutPresenter(
matrixClient: MatrixClient = FakeMatrixClient(), matrixClient: MatrixClient = FakeMatrixClient(),
encryptionService: EncryptionService = FakeEncryptionService(), encryptionService: EncryptionService = FakeEncryptionService(),
): DefaultDirectLogoutPresenter = DefaultDirectLogoutPresenter( ): DirectLogoutPresenter = DirectLogoutPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
encryptionService = encryptionService, encryptionService = encryptionService,
) )

View file

@ -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.TimelineItemAction
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor 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.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.messagecomposer.MessageComposerState
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState 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.TimelineController
import io.element.android.features.messages.impl.timeline.TimelineEvents 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.TimelinePresenter
import io.element.android.features.messages.impl.timeline.TimelineState 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.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.model.TimelineItem 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.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState 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.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
@ -88,14 +87,14 @@ import timber.log.Timber
class MessagesPresenter @AssistedInject constructor( class MessagesPresenter @AssistedInject constructor(
@Assisted private val navigator: MessagesNavigator, @Assisted private val navigator: MessagesNavigator,
private val room: MatrixRoom, private val room: MatrixRoom,
private val composerPresenter: MessageComposerPresenter, private val composerPresenter: Presenter<MessageComposerState>,
private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, private val voiceMessageComposerPresenter: Presenter<VoiceMessageComposerState>,
timelinePresenterFactory: TimelinePresenter.Factory, timelinePresenterFactory: TimelinePresenter.Factory,
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>, private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
private val actionListPresenterFactory: ActionListPresenter.Factory, private val actionListPresenterFactory: ActionListPresenter.Factory,
private val customReactionPresenter: CustomReactionPresenter, private val customReactionPresenter: Presenter<CustomReactionState>,
private val reactionSummaryPresenter: ReactionSummaryPresenter, private val reactionSummaryPresenter: Presenter<ReactionSummaryState>,
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter, private val readReceiptBottomSheetPresenter: Presenter<ReadReceiptBottomSheetState>,
private val pinnedMessagesBannerPresenter: Presenter<PinnedMessagesBannerState>, private val pinnedMessagesBannerPresenter: Presenter<PinnedMessagesBannerState>,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val snackbarDispatcher: SnackbarDispatcher, private val snackbarDispatcher: SnackbarDispatcher,

View file

@ -12,12 +12,22 @@ import dagger.Binds
import dagger.Module import dagger.Module
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter 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.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.PinnedMessagesBannerPresenter
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState 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.TimelineProtectionPresenter
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState 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.TypingNotificationPresenter
import io.element.android.features.messages.impl.typing.TypingNotificationState 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.architecture.Presenter
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
@ -35,4 +45,19 @@ interface MessagesModule {
@Binds @Binds
fun bindTimelineProtectionPresenter(presenter: TimelineProtectionPresenter): Presenter<TimelineProtectionState> fun bindTimelineProtectionPresenter(presenter: TimelineProtectionPresenter): Presenter<TimelineProtectionState>
@Binds
fun bindMessageComposerPresenter(presenter: MessageComposerPresenter): Presenter<MessageComposerState>
@Binds
fun bindVoiceMessageComposerPresenter(presenter: VoiceMessageComposerPresenter): Presenter<VoiceMessageComposerState>
@Binds
fun bindCustomReactionPresenter(presenter: CustomReactionPresenter): Presenter<CustomReactionState>
@Binds
fun bindReactionSummaryPresenter(presenter: ReactionSummaryPresenter): Presenter<ReactionSummaryState>
@Binds
fun bindReadReceiptBottomSheetPresenter(presenter: ReadReceiptBottomSheetPresenter): Presenter<ReadReceiptBottomSheetState>
} }

View file

@ -33,6 +33,7 @@ fun aMessageComposerState(
canCreatePoll: Boolean = true, canCreatePoll: Boolean = true,
attachmentsState: AttachmentsState = AttachmentsState.None, attachmentsState: AttachmentsState = AttachmentsState.None,
suggestions: ImmutableList<ResolvedSuggestion> = persistentListOf(), suggestions: ImmutableList<ResolvedSuggestion> = persistentListOf(),
eventSink: (MessageComposerEvents) -> Unit = {},
) = MessageComposerState( ) = MessageComposerState(
textEditorState = textEditorState, textEditorState = textEditorState,
isFullScreen = isFullScreen, isFullScreen = isFullScreen,
@ -44,5 +45,5 @@ fun aMessageComposerState(
attachmentsState = attachmentsState, attachmentsState = attachmentsState,
suggestions = suggestions, suggestions = suggestions,
resolveMentionDisplay = { _, _ -> TextDisplay.Plain }, resolveMentionDisplay = { _, _ -> TextDisplay.Plain },
eventSink = {}, eventSink = eventSink,
) )

View file

@ -7,33 +7,23 @@
package io.element.android.features.messages.impl package io.element.android.features.messages.impl
import android.net.Uri
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.PinUnpinAction 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.ActionListState
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter 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.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.aMessageEvent
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
import io.element.android.features.messages.impl.messagecomposer.FakeRoomAliasSuggestionsDataSource import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
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.pinned.banner.aLoadedPinnedMessagesBannerState 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.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.TimelinePresenter
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter import io.element.android.features.messages.impl.timeline.createTimelinePresenter
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.model.event.TimelineItemFileContent 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.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent 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.aTimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent 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.timeline.protection.aTimelineProtectionState
import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
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.test.timeline.FakeHtmlConverterProvider import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.features.poll.api.actions.EndPollAction 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.FakeEndPollAction
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
import io.element.android.libraries.architecture.AsyncData 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.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.mimetype.MimeTypes 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.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId 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.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMembershipState 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_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_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_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_SESSION_ID_2
import io.element.android.libraries.matrix.test.A_THROWABLE 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.A_UNIQUE_ID
import io.element.android.libraries.matrix.test.core.aBuildMeta 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.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom 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.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.timeline.FakeTimeline 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.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
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.MessageComposerMode 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.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.consumeItemsUntilTimeout 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.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -123,8 +97,6 @@ class MessagesPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
private val mockMediaUrl: Uri = mockk("localMediaUri")
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = createMessagesPresenter() val presenter = createMessagesPresenter()
@ -212,15 +184,13 @@ class MessagesPresenterTest {
}.test { }.test {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) initialState.eventSink(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)
assert(toggleReactionSuccess) assert(toggleReactionSuccess)
.isCalledOnce() .isCalledOnce()
.with(value("👍"), value(A_UNIQUE_ID)) .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) assert(toggleReactionFailure)
.isCalledOnce() .isCalledOnce()
.with(value("👍"), value(A_UNIQUE_ID)) .with(value("👍"), value(A_UNIQUE_ID))
@ -248,15 +218,16 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) initialState.eventSink(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) initialState.eventSink(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
assert(toggleReactionSuccess) assert(toggleReactionSuccess)
.isCalledExactly(2) .isCalledExactly(2)
.withSequence( .withSequence(
listOf(value("👍"), value(A_UNIQUE_ID)), listOf(value("👍"), value(A_UNIQUE_ID)),
listOf(value("👍"), value(A_UNIQUE_ID)), listOf(value("👍"), value(A_UNIQUE_ID)),
) )
skipItems(1)
} }
} }
@ -267,9 +238,8 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() 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(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
assertThat(navigator.onForwardEventClickedCount).isEqualTo(1) assertThat(navigator.onForwardEventClickedCount).isEqualTo(1)
} }
@ -283,9 +253,9 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Copy, event)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Copy, event))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) skipItems(2)
assertThat(clipboardHelper.clipboardContents).isEqualTo((event.content as TimelineItemTextContent).body) assertThat(clipboardHelper.clipboardContents).isEqualTo((event.content as TimelineItemTextContent).body)
} }
} }
@ -310,24 +280,33 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.CopyLink, event)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.CopyLink, event))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) skipItems(2)
assertThat(clipboardHelper.clipboardContents).isEqualTo("a link") assertThat(clipboardHelper.clipboardContents).isEqualTo("a link")
} }
} }
@Test @Test
fun `present - handle action reply`() = runTest { fun `present - handle action reply`() = runTest {
val presenter = createMessagesPresenter() val composerRecorder = EventsRecorder<MessageComposerEvents>()
val presenter = createMessagesPresenter(
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent())) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent()))
val finalState = awaitItem() awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) composerRecorder.assertSingle(
assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) MessageComposerEvents.SetMode(
composerMode = MessageComposerMode.Reply(
replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID),
hideImage = false,
)
)
)
} }
} }
@ -337,21 +316,22 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null)))
assertThat(initialState.actionListState.target).isEqualTo(ActionListState.Target.None) skipItems(1)
// Otherwise we would have some extra items here
ensureAllEventsConsumed()
} }
} }
@Test @Test
fun `present - handle action reply to an image media message`() = runTest { fun `present - handle action reply to an image media message`() = runTest {
val presenter = createMessagesPresenter() val composerRecorder = EventsRecorder<MessageComposerEvents>()
val presenter = createMessagesPresenter(
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
val mediaMessage = aMessageEvent( val mediaMessage = aMessageEvent(
content = TimelineItemImageContent( content = TimelineItemImageContent(
body = "image.jpg", body = "image.jpg",
@ -368,22 +348,29 @@ class MessagesPresenterTest {
formattedFileSize = "4MB" formattedFileSize = "4MB"
) )
) )
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage))
val finalState = awaitItem() awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) composerRecorder.assertSingle(
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply MessageComposerEvents.SetMode(
assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) composerMode = MessageComposerMode.Reply(
assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID),
hideImage = false,
)
)
)
} }
} }
@Test @Test
fun `present - handle action reply to a video media message`() = runTest { fun `present - handle action reply to a video media message`() = runTest {
val presenter = createMessagesPresenter() val composerRecorder = EventsRecorder<MessageComposerEvents>()
val presenter = createMessagesPresenter(
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
val mediaMessage = aMessageEvent( val mediaMessage = aMessageEvent(
content = TimelineItemVideoContent( content = TimelineItemVideoContent(
body = "video.mp4", body = "video.mp4",
@ -401,22 +388,29 @@ class MessagesPresenterTest {
formattedFileSize = "50MB" formattedFileSize = "50MB"
) )
) )
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage))
val finalState = awaitItem() awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) composerRecorder.assertSingle(
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply MessageComposerEvents.SetMode(
assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) composerMode = MessageComposerMode.Reply(
assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID),
hideImage = false,
)
)
)
} }
} }
@Test @Test
fun `present - handle action reply to a file media message`() = runTest { fun `present - handle action reply to a file media message`() = runTest {
val presenter = createMessagesPresenter() val composerRecorder = EventsRecorder<MessageComposerEvents>()
val presenter = createMessagesPresenter(
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
val mediaMessage = aMessageEvent( val mediaMessage = aMessageEvent(
content = TimelineItemFileContent( content = TimelineItemFileContent(
body = "file.pdf", body = "file.pdf",
@ -427,26 +421,40 @@ class MessagesPresenterTest {
fileExtension = "pdf", fileExtension = "pdf",
) )
) )
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage))
val finalState = awaitItem() awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) composerRecorder.assertSingle(
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply MessageComposerEvents.SetMode(
assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) composerMode = MessageComposerMode.Reply(
assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID),
hideImage = false,
)
)
)
} }
} }
@Test @Test
fun `present - handle action edit`() = runTest { fun `present - handle action edit`() = runTest {
val presenter = createMessagesPresenter() val composerRecorder = EventsRecorder<MessageComposerEvents>()
val presenter = createMessagesPresenter(
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent())) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent()))
val finalState = awaitItem() awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Edit::class.java) composerRecorder.assertSingle(
assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) 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) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent()))) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent())))
awaitItem()
assertThat(navigator.onEditPollClickedCount).isEqualTo(1) assertThat(navigator.onEditPollClickedCount).isEqualTo(1)
} }
} }
@ -470,9 +479,9 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
endPollAction.verifyExecutionCount(0) endPollAction.verifyExecutionCount(0)
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent()))) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent())))
delay(1) delay(1)
endPollAction.verifyExecutionCount(1) endPollAction.verifyExecutionCount(1)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
@ -496,16 +505,17 @@ class MessagesPresenterTest {
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(Unit) } val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(Unit) }
liveTimeline.redactEventLambda = redactEventLambda liveTimeline.redactEventLambda = redactEventLambda
val presenter = createMessagesPresenter(
val presenter = createMessagesPresenter(matrixRoom = matrixRoom, coroutineDispatchers = coroutineDispatchers) matrixRoom = matrixRoom,
coroutineDispatchers = coroutineDispatchers,
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
val messageEvent = aMessageEvent() val messageEvent = aMessageEvent()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, messageEvent)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Redact, messageEvent))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) awaitItem()
assert(redactEventLambda) assert(redactEventLambda)
.isCalledOnce() .isCalledOnce()
.with(value(messageEvent.eventId), value(messageEvent.transactionId), value(null)) .with(value(messageEvent.eventId), value(messageEvent.transactionId), value(null))
@ -519,9 +529,8 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() 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(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
assertThat(navigator.onReportContentClickedCount).isEqualTo(1) assertThat(navigator.onReportContentClickedCount).isEqualTo(1)
} }
@ -533,9 +542,8 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.Dismiss) initialState.eventSink(MessagesEvents.Dismiss)
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
} }
} }
@ -547,9 +555,8 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() 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(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
assertThat(navigator.onShowEventDebugInfoClickedCount).isEqualTo(1) assertThat(navigator.onShowEventDebugInfoClickedCount).isEqualTo(1)
} }
@ -572,21 +579,18 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
// Initially the composer doesn't have focus, so we don't show the alert // Initially the composer doesn't have focus, so we don't show the alert
assertThat(initialState.showReinvitePrompt).isFalse() assertThat(initialState.showReinvitePrompt).isFalse()
// When the input field is focused we show the alert // When the input field is focused we show the alert
initialState.composerState.textEditorState.requestFocus() (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
val focusedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state -> skipItems(1)
state.showReinvitePrompt val focusedState = awaitItem()
}.last()
assertThat(focusedState.showReinvitePrompt).isTrue() assertThat(focusedState.showReinvitePrompt).isTrue()
// If it's dismissed then we stop showing the alert // If it's dismissed then we stop showing the alert
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel)) initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel))
val dismissedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state -> skipItems(1)
!state.showReinvitePrompt val dismissedState = awaitItem()
}.last()
assertThat(dismissedState.showReinvitePrompt).isFalse() assertThat(dismissedState.showReinvitePrompt).isFalse()
} }
} }
@ -608,9 +612,9 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
assertThat(initialState.showReinvitePrompt).isFalse() assertThat(initialState.showReinvitePrompt).isFalse()
initialState.composerState.textEditorState.requestFocus() (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
val focusedState = awaitItem() val focusedState = awaitItem()
assertThat(focusedState.showReinvitePrompt).isFalse() assertThat(focusedState.showReinvitePrompt).isFalse()
} }
@ -633,9 +637,9 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
assertThat(initialState.showReinvitePrompt).isFalse() assertThat(initialState.showReinvitePrompt).isFalse()
initialState.composerState.textEditorState.requestFocus() (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
val focusedState = awaitItem() val focusedState = awaitItem()
assertThat(focusedState.showReinvitePrompt).isFalse() assertThat(focusedState.showReinvitePrompt).isFalse()
} }
@ -799,7 +803,8 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val state = awaitFirstItem() skipItems(1)
val state = awaitItem()
assertThat(state.userEventPermissions.canSendMessage).isTrue() assertThat(state.userEventPermissions.canSendMessage).isTrue()
} }
} }
@ -826,9 +831,7 @@ class MessagesPresenterTest {
}.test { }.test {
// Default value // Default value
assertThat(awaitItem().userEventPermissions.canSendMessage).isTrue() assertThat(awaitItem().userEventPermissions.canSendMessage).isTrue()
skipItems(1)
assertThat(awaitItem().userEventPermissions.canSendMessage).isFalse() assertThat(awaitItem().userEventPermissions.canSendMessage).isFalse()
cancelAndIgnoreRemainingEvents()
} }
} }
@ -876,21 +879,27 @@ class MessagesPresenterTest {
@Test @Test
fun `present - handle action reply to a poll`() = runTest { fun `present - handle action reply to a poll`() = runTest {
val presenter = createMessagesPresenter() val composerRecorder = EventsRecorder<MessageComposerEvents>()
val presenter = createMessagesPresenter(
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
val poll = aMessageEvent( val poll = aMessageEvent(
content = aTimelineItemPollContent() content = aTimelineItemPollContent()
) )
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, poll)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, poll))
val finalState = awaitItem() skipItems(1)
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) composerRecorder.assertSingle(
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply MessageComposerEvents.SetMode(
composerMode = MessageComposerMode.Reply(
assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID),
assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) hideImage = false,
)
)
)
} }
} }
@ -916,15 +925,16 @@ class MessagesPresenterTest {
val messageEvent = aMessageEvent( val messageEvent = aMessageEvent(
content = aTimelineItemTextContent() content = aTimelineItemTextContent()
) )
val initialState = awaitFirstItem() val initialState = awaitItem()
timeline.pinEventLambda = successPinEventLambda 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)) assert(successPinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
timeline.pinEventLambda = failurePinEventLambda 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)) assert(failurePinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
skipItems(1)
assertThat(awaitItem().snackbarMessage).isNotNull() assertThat(awaitItem().snackbarMessage).isNotNull()
assertThat(analyticsService.capturedEvents).containsExactly( assertThat(analyticsService.capturedEvents).containsExactly(
PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline), PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline),
@ -955,15 +965,16 @@ class MessagesPresenterTest {
val messageEvent = aMessageEvent( val messageEvent = aMessageEvent(
content = aTimelineItemTextContent() content = aTimelineItemTextContent()
) )
val initialState = awaitFirstItem() val initialState = awaitItem()
timeline.unpinEventLambda = successUnpinEventLambda 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)) assert(successUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
timeline.unpinEventLambda = failureUnpinEventLambda 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)) assert(failureUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
skipItems(1)
assertThat(awaitItem().snackbarMessage).isNotNull() assertThat(awaitItem().snackbarMessage).isNotNull()
assertThat(analyticsService.capturedEvents).containsExactly( assertThat(analyticsService.capturedEvents).containsExactly(
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline), PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline),
@ -972,12 +983,6 @@ class MessagesPresenterTest {
} }
} }
private suspend fun <T> ReceiveTurbine<T>.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( private fun TestScope.createMessagesPresenter(
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
matrixRoom: MatrixRoom = FakeMatrixRoom( matrixRoom: MatrixRoom = FakeMatrixRoom(
@ -993,83 +998,34 @@ class MessagesPresenterTest {
navigator: FakeMessagesNavigator = FakeMessagesNavigator(), navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
endPollAction: EndPollAction = FakeEndPollAction(), endPollAction: EndPollAction = FakeEndPollAction(),
permalinkParser: PermalinkParser = FakePermalinkParser(), permalinkParser: PermalinkParser = FakePermalinkParser(),
messageComposerPresenter: Presenter<MessageComposerState> = Presenter {
aMessageComposerState(
// Use TextEditorState.Markdown, so that we can request focus manually.
textEditorState = TextEditorState.Markdown(MarkdownTextEditorState(initialText = "", initialFocus = false))
)
},
actionListEventSink: (ActionListEvents) -> Unit = {},
): MessagesPresenter { ): 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 { val timelinePresenterFactory = object : TimelinePresenter.Factory {
override fun create(navigator: MessagesNavigator): TimelinePresenter { override fun create(navigator: MessagesNavigator): TimelinePresenter {
return timelinePresenter return createTimelinePresenter(
endPollAction = endPollAction,
)
} }
} }
val featureFlagService = FakeFeatureFlagService() val featureFlagService = FakeFeatureFlagService()
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
return MessagesPresenter( return MessagesPresenter(
room = matrixRoom, room = matrixRoom,
composerPresenter = messageComposerPresenter, composerPresenter = messageComposerPresenter,
voiceMessageComposerPresenter = voiceMessageComposerPresenter, voiceMessageComposerPresenter = { aVoiceMessageComposerState() },
timelinePresenterFactory = timelinePresenterFactory, timelinePresenterFactory = timelinePresenterFactory,
timelineProtectionPresenter = { aTimelineProtectionState() }, timelineProtectionPresenter = { aTimelineProtectionState() },
actionListPresenterFactory = FakeActionListPresenter.Factory, actionListPresenterFactory = FakeActionListPresenter.Factory(actionListEventSink),
customReactionPresenter = customReactionPresenter, customReactionPresenter = { aCustomReactionState() },
reactionSummaryPresenter = reactionSummaryPresenter, reactionSummaryPresenter = { aReactionSummaryState() },
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter, readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() },
pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() }, pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() },
networkMonitor = FakeNetworkMonitor(), networkMonitor = FakeNetworkMonitor(),
snackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher = SnackbarDispatcher(),

View file

@ -10,15 +10,15 @@ package io.element.android.features.messages.impl.actionlist
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
class FakeActionListPresenter : ActionListPresenter { class FakeActionListPresenter(private val eventSink: (ActionListEvents) -> Unit = {}) : ActionListPresenter {
object Factory : ActionListPresenter.Factory { class Factory(private val eventSink: (ActionListEvents) -> Unit = {}) : ActionListPresenter.Factory {
override fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter { override fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter {
return FakeActionListPresenter() return FakeActionListPresenter(eventSink)
} }
} }
@Composable @Composable
override fun present(): ActionListState { override fun present(): ActionListState {
return anActionListState() return anActionListState(eventSink = eventSink)
} }
} }

View file

@ -312,7 +312,7 @@ class PinnedMessagesListPresenterTest {
timelineProvider = timelineProvider, timelineProvider = timelineProvider,
timelineProtectionPresenter = { aTimelineProtectionState() }, timelineProtectionPresenter = { aTimelineProtectionState() },
snackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher = SnackbarDispatcher(),
actionListPresenterFactory = FakeActionListPresenter.Factory, actionListPresenterFactory = FakeActionListPresenter.Factory(),
analyticsService = analyticsService, analyticsService = analyticsService,
appCoroutineScope = this, appCoroutineScope = this,
) )

View file

@ -656,34 +656,34 @@ import kotlin.time.Duration.Companion.seconds
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T { private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
return awaitItem() return awaitItem()
} }
}
private fun TestScope.createTimelinePresenter(
timeline: Timeline = FakeTimeline(), internal fun TestScope.createTimelinePresenter(
room: FakeMatrixRoom = FakeMatrixRoom( timeline: Timeline = FakeTimeline(),
liveTimeline = timeline, room: FakeMatrixRoom = FakeMatrixRoom(
canUserSendMessageResult = { _, _ -> Result.success(true) } liveTimeline = timeline,
), canUserSendMessageResult = { _, _ -> Result.success(true) }
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), ),
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
endPollAction: EndPollAction = FakeEndPollAction(), messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), endPollAction: EndPollAction = FakeEndPollAction(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
): TimelinePresenter { timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
return TimelinePresenter( ): TimelinePresenter {
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), return TimelinePresenter(
room = room, timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
dispatchers = testCoroutineDispatchers(), room = room,
appScope = this, dispatchers = testCoroutineDispatchers(),
navigator = messagesNavigator, appScope = this,
redactedVoiceMessageManager = redactedVoiceMessageManager, navigator = messagesNavigator,
endPollAction = endPollAction, redactedVoiceMessageManager = redactedVoiceMessageManager,
sendPollResponseAction = sendPollResponseAction, endPollAction = endPollAction,
sessionPreferencesStore = sessionPreferencesStore, sendPollResponseAction = sendPollResponseAction,
timelineItemIndexer = timelineItemIndexer, sessionPreferencesStore = sessionPreferencesStore,
timelineController = TimelineController(room), timelineItemIndexer = timelineItemIndexer,
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, timelineController = TimelineController(room),
typingNotificationPresenter = { aTypingNotificationState() }, resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
) typingNotificationPresenter = { aTypingNotificationState() },
} )
} }

View file

@ -83,7 +83,6 @@ dependencies {
testImplementation(projects.features.logout.test) testImplementation(projects.features.logout.test)
testImplementation(projects.libraries.indicator.impl) testImplementation(projects.libraries.indicator.impl)
testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.fullscreenintent.test)
testImplementation(projects.features.logout.impl) testImplementation(projects.features.logout.impl)
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.test) testImplementation(projects.services.toolbox.test)

View file

@ -8,19 +8,19 @@
package io.element.android.features.preferences.impl.analytics package io.element.android.features.preferences.impl.analytics
import androidx.compose.runtime.Composable 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 io.element.android.libraries.architecture.Presenter
import javax.inject.Inject import javax.inject.Inject
class AnalyticsSettingsPresenter @Inject constructor( class AnalyticsSettingsPresenter @Inject constructor(
private val analyticsPresenter: AnalyticsPreferencesPresenter, private val analyticsPreferencesPresenter: Presenter<AnalyticsPreferencesState>,
) : Presenter<AnalyticsSettingsState> { ) : Presenter<AnalyticsSettingsState> {
@Composable @Composable
override fun present(): AnalyticsSettingsState { override fun present(): AnalyticsSettingsState {
val analyticsState = analyticsPresenter.present() val analyticsPreferencesState = analyticsPreferencesPresenter.present()
return AnalyticsSettingsState( return AnalyticsSettingsState(
analyticsState = analyticsState, analyticsPreferencesState = analyticsPreferencesState,
) )
} }
} }

View file

@ -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. // Do not use default value, so no member get forgotten in the presenters.
data class AnalyticsSettingsState( data class AnalyticsSettingsState(
val analyticsState: AnalyticsPreferencesState, val analyticsPreferencesState: AnalyticsPreferencesState,
) )

View file

@ -18,5 +18,5 @@ open class AnalyticsSettingsStateProvider : PreviewParameterProvider<AnalyticsSe
} }
fun aAnalyticsSettingsState() = AnalyticsSettingsState( fun aAnalyticsSettingsState() = AnalyticsSettingsState(
analyticsState = aAnalyticsPreferencesState(), analyticsPreferencesState = aAnalyticsPreferencesState(),
) )

View file

@ -29,7 +29,7 @@ fun AnalyticsSettingsView(
title = stringResource(id = CommonStrings.common_analytics) title = stringResource(id = CommonStrings.common_analytics)
) { ) {
AnalyticsPreferencesView( AnalyticsPreferencesView(
state = state.analyticsState, state = state.analyticsPreferencesState,
) )
} }
} }

View file

@ -22,7 +22,7 @@ import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.logout.api.LogoutUseCase import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
@ -44,7 +44,7 @@ class DeveloperSettingsPresenter @Inject constructor(
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase, private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
private val clearCacheUseCase: ClearCacheUseCase, private val clearCacheUseCase: ClearCacheUseCase,
private val rageshakePresenter: RageshakePreferencesPresenter, private val rageshakePresenter: Presenter<RageshakePreferencesState>,
private val appPreferencesStore: AppPreferencesStore, private val appPreferencesStore: AppPreferencesStore,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val logoutUseCase: LogoutUseCase, private val logoutUseCase: LogoutUseCase,

View file

@ -22,7 +22,7 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingStateNoSuccess 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.MatrixClient
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@ -47,7 +47,7 @@ class NotificationSettingsPresenter @Inject constructor(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val pushService: PushService, private val pushService: PushService,
private val systemNotificationsEnabledProvider: SystemNotificationsEnabledProvider, private val systemNotificationsEnabledProvider: SystemNotificationsEnabledProvider,
private val fullScreenIntentPermissionsPresenter: FullScreenIntentPermissionsPresenter, private val fullScreenIntentPermissionsPresenter: Presenter<FullScreenIntentPermissionsState>,
) : Presenter<NotificationSettingsState> { ) : Presenter<NotificationSettingsState> {
@Composable @Composable
override fun present(): NotificationSettingsState { override fun present(): NotificationSettingsState {

View file

@ -11,6 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState 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 io.element.android.libraries.matrix.api.room.RoomNotificationMode
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -87,15 +88,3 @@ fun aInvalidNotificationSettingsState(
fullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(), fullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
eventSink = eventSink, eventSink = eventSink,
) )
internal fun aFullScreenIntentPermissionsState(
permissionGranted: Boolean = true,
shouldDisplay: Boolean = false,
openFullScreenIntentSettings: () -> Unit = {},
dismissFullScreenIntentBanner: () -> Unit = {},
) = FullScreenIntentPermissionsState(
permissionGranted = permissionGranted,
shouldDisplayBanner = shouldDisplay,
openFullScreenIntentSettings = openFullScreenIntentSettings,
dismissFullScreenIntentBanner = dismissFullScreenIntentBanner,
)

View file

@ -16,7 +16,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.features.preferences.impl.utils.ShowDeveloperSettingsProvider
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@ -42,7 +42,7 @@ class PreferencesRootPresenter @Inject constructor(
private val snackbarDispatcher: SnackbarDispatcher, private val snackbarDispatcher: SnackbarDispatcher,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val indicatorService: IndicatorService, private val indicatorService: IndicatorService,
private val directLogoutPresenter: DirectLogoutPresenter, private val directLogoutPresenter: Presenter<DirectLogoutState>,
private val showDeveloperSettingsProvider: ShowDeveloperSettingsProvider, private val showDeveloperSettingsProvider: ShowDeveloperSettingsProvider,
) : Presenter<PreferencesRootState> { ) : Presenter<PreferencesRootState> {
@Composable @Composable

View file

@ -11,9 +11,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.analytics.impl.preferences.DefaultAnalyticsPreferencesPresenter import io.element.android.features.analytics.api.preferences.aAnalyticsPreferencesState
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@ -25,15 +23,14 @@ class AnalyticsSettingsPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val analyticsPresenter = DefaultAnalyticsPreferencesPresenter(FakeAnalyticsService(), aBuildMeta())
val presenter = AnalyticsSettingsPresenter( val presenter = AnalyticsSettingsPresenter(
analyticsPresenter, analyticsPreferencesPresenter = { aAnalyticsPreferencesState() },
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.analyticsState.isEnabled).isFalse() assertThat(initialState.analyticsPreferencesState.isEnabled).isFalse()
} }
} }
} }

View file

@ -15,9 +15,7 @@ import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.logout.test.FakeLogoutUseCase 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.FakeClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.core.meta.BuildType
@ -54,7 +52,7 @@ class DeveloperSettingsPresenterTest {
val loadedState = awaitItem() val loadedState = awaitItem()
assertThat(loadedState.rageshakeState.isEnabled).isFalse() assertThat(loadedState.rageshakeState.isEnabled).isFalse()
assertThat(loadedState.rageshakeState.isSupported).isTrue() assertThat(loadedState.rageshakeState.isSupported).isTrue()
assertThat(loadedState.rageshakeState.sensitivity).isEqualTo(1.0f) assertThat(loadedState.rageshakeState.sensitivity).isEqualTo(0.3f)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@ -105,9 +103,8 @@ class DeveloperSettingsPresenterTest {
@Test @Test
fun `present - clear cache`() = runTest { fun `present - clear cache`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val clearCacheUseCase = FakeClearCacheUseCase() val clearCacheUseCase = FakeClearCacheUseCase()
val presenter = createDeveloperSettingsPresenter(clearCacheUseCase = clearCacheUseCase, rageshakePresenter = rageshakePresenter) val presenter = createDeveloperSettingsPresenter(clearCacheUseCase = clearCacheUseCase)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -202,7 +199,6 @@ class DeveloperSettingsPresenterTest {
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(), cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(),
clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(), clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(),
rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()),
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
buildMeta: BuildMeta = aBuildMeta(), buildMeta: BuildMeta = aBuildMeta(),
logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }) logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" })
@ -211,7 +207,7 @@ class DeveloperSettingsPresenterTest {
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
computeCacheSizeUseCase = cacheSizeUseCase, computeCacheSizeUseCase = cacheSizeUseCase,
clearCacheUseCase = clearCacheUseCase, clearCacheUseCase = clearCacheUseCase,
rageshakePresenter = rageshakePresenter, rageshakePresenter = { aRageshakePreferencesState() },
appPreferencesStore = preferencesStore, appPreferencesStore = preferencesStore,
buildMeta = buildMeta, buildMeta = buildMeta,
logoutUseCase = logoutUseCase, logoutUseCase = logoutUseCase,

View file

@ -12,7 +12,8 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncData 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.MatrixClient
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_THROWABLE
@ -263,12 +264,11 @@ class NotificationSettingsPresenterTest {
@Test @Test
fun `present - RefreshSystemNotificationsEnabled also refreshes fullScreenIntentState`() = runTest { fun `present - RefreshSystemNotificationsEnabled also refreshes fullScreenIntentState`() = runTest {
val fullScreenIntentPermissionsPresenter = FakeFullScreenIntentPermissionsPresenter().apply { var lambdaResult = aFullScreenIntentPermissionsState(permissionGranted = false)
state = state.copy(permissionGranted = false) val fullScreenIntentPermissionsStateLambda = { lambdaResult }
}
val presenter = createNotificationSettingsPresenter( val presenter = createNotificationSettingsPresenter(
pushService = createFakePushService(), pushService = createFakePushService(),
fullScreenIntentPermissionsPresenter = fullScreenIntentPermissionsPresenter, fullScreenIntentPermissionsStateLambda = fullScreenIntentPermissionsStateLambda,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -277,7 +277,7 @@ class NotificationSettingsPresenterTest {
assertThat(initialState.fullScreenIntentPermissionsState.permissionGranted).isFalse() assertThat(initialState.fullScreenIntentPermissionsState.permissionGranted).isFalse()
// Change the notification settings // Change the notification settings
fullScreenIntentPermissionsPresenter.state = fullScreenIntentPermissionsPresenter.state.copy(permissionGranted = true) lambdaResult = lambdaResult.copy(permissionGranted = true)
// Check it's not changed unless we refresh // Check it's not changed unless we refresh
expectNoEvents() expectNoEvents()
@ -336,7 +336,7 @@ class NotificationSettingsPresenterTest {
private fun createNotificationSettingsPresenter( private fun createNotificationSettingsPresenter(
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
pushService: PushService = FakePushService(), pushService: PushService = FakePushService(),
fullScreenIntentPermissionsPresenter: FakeFullScreenIntentPermissionsPresenter = FakeFullScreenIntentPermissionsPresenter() fullScreenIntentPermissionsStateLambda: () -> FullScreenIntentPermissionsState = { aFullScreenIntentPermissionsState() },
): NotificationSettingsPresenter { ): NotificationSettingsPresenter {
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
return NotificationSettingsPresenter( return NotificationSettingsPresenter(
@ -345,7 +345,7 @@ class NotificationSettingsPresenterTest {
matrixClient = matrixClient, matrixClient = matrixClient,
pushService = pushService, pushService = pushService,
systemNotificationsEnabledProvider = FakeSystemNotificationsEnabledProvider(), systemNotificationsEnabledProvider = FakeSystemNotificationsEnabledProvider(),
fullScreenIntentPermissionsPresenter = fullScreenIntentPermissionsPresenter, fullScreenIntentPermissionsPresenter = { fullScreenIntentPermissionsStateLambda() },
) )
} }
} }

View file

@ -7,16 +7,13 @@
package io.element.android.features.preferences.impl.root package io.element.android.features.preferences.impl.root
import androidx.compose.runtime.Composable
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.direct.DirectLogoutPresenter import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider 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.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
@ -38,12 +35,6 @@ class PreferencesRootPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
private val aDirectLogoutState = DirectLogoutState(
canDoDirectSignOut = true,
logoutAction = AsyncAction.Uninitialized,
eventSink = {},
)
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val matrixClient = FakeMatrixClient(canDeactivateAccountResult = { true }) val matrixClient = FakeMatrixClient(canDeactivateAccountResult = { true })
@ -78,7 +69,7 @@ class PreferencesRootPresenterTest {
assertThat(loadedState.showLockScreenSettings).isTrue() assertThat(loadedState.showLockScreenSettings).isTrue()
assertThat(loadedState.showNotificationSettings).isTrue() assertThat(loadedState.showNotificationSettings).isTrue()
assertThat(loadedState.canDeactivateAccount).isTrue() assertThat(loadedState.canDeactivateAccount).isTrue()
assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState) assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState())
assertThat(loadedState.snackbarMessage).isNull() assertThat(loadedState.snackbarMessage).isNull()
} }
} }
@ -148,10 +139,7 @@ class PreferencesRootPresenterTest {
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
encryptionService = FakeEncryptionService(), encryptionService = FakeEncryptionService(),
), ),
directLogoutPresenter = object : DirectLogoutPresenter { directLogoutPresenter = { aDirectLogoutState() },
@Composable
override fun present() = aDirectLogoutState
},
showDeveloperSettingsProvider = showDeveloperSettingsProvider, showDeveloperSettingsProvider = showDeveloperSettingsProvider,
) )
} }

View file

@ -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<RageshakePreferencesState>
@Binds
fun bindRageshakeDetectionPresenter(presenter: RageshakeDetectionPresenter): Presenter<RageshakeDetectionState>
@Binds
fun bindCrashDetectionPresenter(presenter: CrashDetectionPresenter): Presenter<CrashDetectionState>
}

View file

@ -64,7 +64,6 @@ dependencies {
testImplementation(projects.libraries.usersearch.test) testImplementation(projects.libraries.usersearch.test)
testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
testImplementation(projects.features.leaveroom.test)
testImplementation(projects.features.createroom.test) testImplementation(projects.features.createroom.test)
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
testImplementation(libs.androidx.compose.ui.test.junit) testImplementation(libs.androidx.compose.ui.test.junit)

View file

@ -19,7 +19,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.leaveroom.api.LeaveRoomEvent 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.messages.api.pinned.IsPinnedMessagesFeatureEnabled
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
@ -56,7 +56,7 @@ class RoomDetailsPresenter @Inject constructor(
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val notificationSettingsService: NotificationSettingsService, private val notificationSettingsService: NotificationSettingsService,
private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory, private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory,
private val leaveRoomPresenter: LeaveRoomPresenter, private val leaveRoomPresenter: Presenter<LeaveRoomState>,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled, private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled,

View file

@ -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<RoomMembersModerationState>
}

View file

@ -21,7 +21,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents 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.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@ -40,7 +40,7 @@ class RoomMemberListPresenter @AssistedInject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
private val roomMemberListDataSource: RoomMemberListDataSource, private val roomMemberListDataSource: RoomMemberListDataSource,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
private val roomMembersModerationPresenter: RoomMembersModerationPresenter, private val roomMembersModerationPresenter: Presenter<RoomMembersModerationState>,
@Assisted private val navigator: RoomMemberListNavigator, @Assisted private val navigator: RoomMemberListNavigator,
) : Presenter<RoomMemberListState> { ) : Presenter<RoomMemberListState> {
@AssistedFactory @AssistedFactory
@ -136,7 +136,7 @@ class RoomMemberListPresenter @AssistedInject constructor(
is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active
is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query
is RoomMemberListEvents.RoomMemberSelected -> coroutineScope.launch { is RoomMemberListEvents.RoomMemberSelected -> coroutineScope.launch {
if (roomMembersModerationPresenter.canDisplayModerationActions()) { if (roomModerationState.canDisplayModerationActions) {
roomModerationState.eventSink(RoomMembersModerationEvents.SelectRoomMember(event.roomMember)) roomModerationState.eventSink(RoomMembersModerationEvents.SelectRoomMember(event.roomMember))
} else { } else {
navigator.openRoomMemberDetails(event.roomMember.userId) navigator.openRoomMemberDetails(event.roomMember.userId)

View file

@ -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<RoomMember?>(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<ModerationAction>()) }
val kickUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
val banUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
val unbanUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
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<AsyncAction<Unit>>,
) = runActionAndWaitForMembershipChange(kickUserAction) {
analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember))
room.kickUser(userId).finally { selectedMember = null }
}
private fun CoroutineScope.banUser(
userId: UserId,
banUserAction: MutableState<AsyncAction<Unit>>,
) = runActionAndWaitForMembershipChange(banUserAction) {
analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember))
room.banUser(userId).finally { selectedMember = null }
}
private fun CoroutineScope.unbanUser(
userId: UserId,
unbanUserAction: MutableState<AsyncAction<Unit>>,
) = runActionAndWaitForMembershipChange(unbanUserAction) {
analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember))
room.unbanUser(userId).finally { selectedMember = null }
}
private fun <T> CoroutineScope.runActionAndWaitForMembershipChange(action: MutableState<AsyncAction<T>>, block: suspend () -> Result<T>) {
launch(dispatchers.io) {
action.runUpdatingState {
val result = block()
if (result.isSuccess) {
room.membersStateFlow.drop(1).take(1)
}
result
}
}
}
}

View file

@ -7,20 +7,179 @@
package io.element.android.features.roomdetails.impl.members.moderation 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.AsyncAction
import io.element.android.libraries.architecture.Presenter 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.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<RoomMembersModerationState> { class RoomMembersModerationPresenter @Inject constructor(
suspend fun canDisplayModerationActions(): Boolean private val room: MatrixRoom,
private val dispatchers: CoroutineDispatchers,
private val analyticsService: AnalyticsService,
) : Presenter<RoomMembersModerationState> {
private var selectedMember by mutableStateOf<RoomMember?>(null)
fun dummyState() = RoomMembersModerationState( private suspend fun canBan() = room.canBan().getOrDefault(false)
selectedRoomMember = null, private suspend fun canKick() = room.canKick().getOrDefault(false)
actions = persistentListOf(),
kickUserAsyncAction = AsyncAction.Uninitialized, @Composable
banUserAsyncAction = AsyncAction.Uninitialized, override fun present(): RoomMembersModerationState {
unbanUserAsyncAction = AsyncAction.Uninitialized, val coroutineScope = rememberCoroutineScope()
canDisplayBannedUsers = false, var moderationActions by remember { mutableStateOf(persistentListOf<ModerationAction>()) }
eventSink = {}
) 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<Unit>) }
val banUserAsyncAction =
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
val unbanUserAsyncAction =
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
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<AsyncAction<Unit>>,
) = runActionAndWaitForMembershipChange(kickUserAction) {
analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember))
room.kickUser(userId).finally { selectedMember = null }
}
private fun CoroutineScope.banUser(
userId: UserId,
banUserAction: MutableState<AsyncAction<Unit>>,
) = runActionAndWaitForMembershipChange(banUserAction) {
analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember))
room.banUser(userId).finally { selectedMember = null }
}
private fun CoroutineScope.unbanUser(
userId: UserId,
unbanUserAction: MutableState<AsyncAction<Unit>>,
) = runActionAndWaitForMembershipChange(unbanUserAction) {
analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember))
room.unbanUser(userId).finally { selectedMember = null }
}
private fun <T> CoroutineScope.runActionAndWaitForMembershipChange(
action: MutableState<AsyncAction<T>>,
block: suspend () -> Result<T>
) {
launch(dispatchers.io) {
action.runUpdatingState {
val result = block()
if (result.isSuccess) {
room.membersStateFlow.drop(1).take(1)
}
result
}
}
}
} }

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class RoomMembersModerationState( data class RoomMembersModerationState(
val canDisplayModerationActions: Boolean,
val selectedRoomMember: RoomMember?, val selectedRoomMember: RoomMember?,
val actions: ImmutableList<ModerationAction>, val actions: ImmutableList<ModerationAction>,
val kickUserAsyncAction: AsyncAction<Unit>, val kickUserAsyncAction: AsyncAction<Unit>,

View file

@ -71,6 +71,7 @@ class RoomMembersModerationStatePreviewProvider : PreviewParameterProvider<RoomM
} }
fun aRoomMembersModerationState( fun aRoomMembersModerationState(
canDisplayModerationActions: Boolean = false,
selectedRoomMember: RoomMember? = null, selectedRoomMember: RoomMember? = null,
actions: List<ModerationAction> = emptyList(), actions: List<ModerationAction> = emptyList(),
kickUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized, kickUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
@ -79,6 +80,7 @@ fun aRoomMembersModerationState(
canDisplayBannedUsers: Boolean = false, canDisplayBannedUsers: Boolean = false,
eventSink: (RoomMembersModerationEvents) -> Unit = {}, eventSink: (RoomMembersModerationEvents) -> Unit = {},
) = RoomMembersModerationState( ) = RoomMembersModerationState(
canDisplayModerationActions = canDisplayModerationActions,
selectedRoomMember = selectedRoomMember, selectedRoomMember = selectedRoomMember,
actions = actions.toPersistentList(), actions = actions.toPersistentList(),
kickUserAsyncAction = kickUserAsyncAction, kickUserAsyncAction = kickUserAsyncAction,

View file

@ -16,8 +16,8 @@ import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.createroom.test.FakeStartDMAction import io.element.android.features.createroom.test.FakeStartDMAction
import io.element.android.features.leaveroom.api.LeaveRoomEvent 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.fake.FakeLeaveRoomPresenter import io.element.android.features.leaveroom.api.aLeaveRoomState
import io.element.android.features.roomdetails.impl.RoomDetailsEvent import io.element.android.features.roomdetails.impl.RoomDetailsEvent
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
import io.element.android.features.roomdetails.impl.RoomDetailsState 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.libraries.matrix.test.room.aRoomInfo
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService 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.FakeLifecycleOwner
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
@ -72,7 +73,7 @@ class RoomDetailsPresenterTest {
private fun TestScope.createRoomDetailsPresenter( private fun TestScope.createRoomDetailsPresenter(
room: MatrixRoom = aMatrixRoom(), room: MatrixRoom = aMatrixRoom(),
leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(), leaveRoomState: LeaveRoomState = aLeaveRoomState(),
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
analyticsService: AnalyticsService = FakeAnalyticsService(), analyticsService: AnalyticsService = FakeAnalyticsService(),
@ -93,7 +94,7 @@ class RoomDetailsPresenterTest {
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
notificationSettingsService = matrixClient.notificationSettingsService(), notificationSettingsService = matrixClient.notificationSettingsService(),
roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory, roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory,
leaveRoomPresenter = leaveRoomPresenter, leaveRoomPresenter = { leaveRoomState },
dispatchers = dispatchers, dispatchers = dispatchers,
isPinnedMessagesFeatureEnabled = { isPinnedMessagesFeatureEnabled }, isPinnedMessagesFeatureEnabled = { isPinnedMessagesFeatureEnabled },
analyticsService = analyticsService, analyticsService = analyticsService,
@ -475,7 +476,7 @@ class RoomDetailsPresenterTest {
@Test @Test
fun `present - leave room event is passed on to leave room presenter`() = runTest { fun `present - leave room event is passed on to leave room presenter`() = runTest {
val leaveRoomPresenter = FakeLeaveRoomPresenter() val leaveRoomEventRecorder = EventsRecorder<LeaveRoomEvent>()
val room = aMatrixRoom( val room = aMatrixRoom(
canInviteResult = { Result.success(true) }, canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) }, canUserJoinCallResult = { Result.success(true) },
@ -483,25 +484,18 @@ class RoomDetailsPresenterTest {
) )
val presenter = createRoomDetailsPresenter( val presenter = createRoomDetailsPresenter(
room = room, room = room,
leaveRoomPresenter = leaveRoomPresenter, leaveRoomState = aLeaveRoomState(eventSink = leaveRoomEventRecorder),
dispatchers = testCoroutineDispatchers() dispatchers = testCoroutineDispatchers()
) )
presenter.test { presenter.test {
awaitItem().eventSink(RoomDetailsEvent.LeaveRoom) awaitItem().eventSink(RoomDetailsEvent.LeaveRoom)
leaveRoomEventRecorder.assertSingle(LeaveRoomEvent.ShowConfirmation(room.roomId))
assertThat(leaveRoomPresenter.events).contains(
LeaveRoomEvent.ShowConfirmation(
room.roomId
)
)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@Test @Test
fun `present - notification mode changes`() = runTest { fun `present - notification mode changes`() = runTest {
val leaveRoomPresenter = FakeLeaveRoomPresenter()
val notificationSettingsService = FakeNotificationSettingsService() val notificationSettingsService = FakeNotificationSettingsService()
val room = aMatrixRoom( val room = aMatrixRoom(
notificationSettingsService = notificationSettingsService, notificationSettingsService = notificationSettingsService,
@ -511,7 +505,6 @@ class RoomDetailsPresenterTest {
) )
val presenter = createRoomDetailsPresenter( val presenter = createRoomDetailsPresenter(
room = room, room = room,
leaveRoomPresenter = leaveRoomPresenter,
notificationSettingsService = notificationSettingsService, notificationSettingsService = notificationSettingsService,
) )
presenter.test { presenter.test {

View file

@ -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.aVictor
import io.element.android.features.roomdetails.impl.members.aWalter 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.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.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.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.UserId 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.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom 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.aRoomInfo
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -194,9 +195,9 @@ class RoomMemberListPresenterTest {
@Test @Test
fun `present - RoomMemberSelected by default opens the room member details through the navigator`() = runTest { fun `present - RoomMemberSelected by default opens the room member details through the navigator`() = runTest {
val navigator = FakeRoomMemberListNavigator() val navigator = FakeRoomMemberListNavigator()
val moderationPresenter = FakeRoomMembersModerationPresenter(canDisplayModerationActions = false) val roomMembersModerationStateLambda = { aRoomMembersModerationState(canDisplayModerationActions = false) }
val presenter = createPresenter( val presenter = createPresenter(
moderationPresenter = moderationPresenter, roomMembersModerationStateLambda = roomMembersModerationStateLambda,
navigator = navigator, navigator = navigator,
matrixRoom = FakeMatrixRoom( matrixRoom = FakeMatrixRoom(
updateMembersResult = { Result.success(Unit) }, updateMembersResult = { Result.success(Unit) },
@ -215,17 +216,15 @@ class RoomMemberListPresenterTest {
@Test @Test
fun `present - RoomMemberSelected will open the moderation options if the current user can use them`() = runTest { fun `present - RoomMemberSelected will open the moderation options if the current user can use them`() = runTest {
val navigator = FakeRoomMemberListNavigator() val navigator = FakeRoomMemberListNavigator()
var selectRoomMemberCallCounts = 0 val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
val capturingState = aRoomMembersModerationState(eventSink = { event -> val roomMembersModerationStateLambda = {
if (event is RoomMembersModerationEvents.SelectRoomMember) { aRoomMembersModerationState(
selectRoomMemberCallCounts++ canDisplayModerationActions = true,
} eventSink = eventsRecorder,
}) )
val moderationPresenter = FakeRoomMembersModerationPresenter(canDisplayModerationActions = true).apply {
givenState(capturingState)
} }
val presenter = createPresenter( val presenter = createPresenter(
moderationPresenter = moderationPresenter, roomMembersModerationStateLambda = roomMembersModerationStateLambda,
navigator = navigator, navigator = navigator,
matrixRoom = FakeMatrixRoom( matrixRoom = FakeMatrixRoom(
updateMembersResult = { Result.success(Unit) }, updateMembersResult = { Result.success(Unit) },
@ -237,7 +236,7 @@ class RoomMemberListPresenterTest {
}.test { }.test {
skipItems(1) skipItems(1)
awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor())) 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) } updateMembersResult = { Result.success(Unit) }
), ),
roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers), roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers),
moderationPresenter: FakeRoomMembersModerationPresenter = FakeRoomMembersModerationPresenter(), roomMembersModerationStateLambda: () -> RoomMembersModerationState = { aRoomMembersModerationState() },
navigator: RoomMemberListNavigator = object : RoomMemberListNavigator {} navigator: RoomMemberListNavigator = object : RoomMemberListNavigator {}
) = RoomMemberListPresenter( ) = RoomMemberListPresenter(
room = matrixRoom, room = matrixRoom,
roomMemberListDataSource = roomMemberListDataSource, roomMemberListDataSource = roomMemberListDataSource,
coroutineDispatchers = coroutineDispatchers, coroutineDispatchers = coroutineDispatchers,
roomMembersModerationPresenter = moderationPresenter, roomMembersModerationPresenter = { roomMembersModerationStateLambda() },
navigator = navigator navigator = navigator
) )

View file

@ -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
}
}

View file

@ -14,9 +14,9 @@ import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.RoomModeration 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.aRoomMember
import io.element.android.features.roomdetails.impl.members.aVictor 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.ModerationAction
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents 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.architecture.AsyncAction
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState 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.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
class DefaultRoomMembersModerationPresenterTest { class RoomMembersModerationPresenterTest {
@Test @Test
fun `canDisplayModerationActions - when room is DM is false`() = runTest { fun `canDisplayModerationActions - when room is DM is false`() = runTest {
val room = FakeMatrixRoom(isDirect = true, isPublic = true, activeMemberCount = 2).apply { val room = FakeMatrixRoom(isDirect = true, isPublic = true, activeMemberCount = 2).apply {
givenRoomInfo(aRoomInfo(isDirect = true, isPublic = false, activeMembersCount = 2)) givenRoomInfo(aRoomInfo(isDirect = true, isPublic = false, activeMembersCount = 2))
} }
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
assertThat(presenter.canDisplayModerationActions()).isFalse() presenter.test {
assertThat(awaitItem().canDisplayModerationActions).isFalse()
}
} }
@Test @Test
@ -51,8 +54,11 @@ class DefaultRoomMembersModerationPresenterTest {
canKickResult = { Result.success(true) }, canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
) )
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
assertThat(presenter.canDisplayModerationActions()).isTrue() presenter.test {
skipItems(1)
assertThat(awaitItem().canDisplayModerationActions).isTrue()
}
} }
@Test @Test
@ -62,8 +68,11 @@ class DefaultRoomMembersModerationPresenterTest {
activeMemberCount = 10, activeMemberCount = 10,
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
) )
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
assertThat(presenter.canDisplayModerationActions()).isTrue() presenter.test {
skipItems(1)
assertThat(awaitItem().canDisplayModerationActions).isTrue()
}
} }
@Test @Test
@ -74,7 +83,7 @@ class DefaultRoomMembersModerationPresenterTest {
userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
) )
val selectedMember = aVictor() val selectedMember = aVictor()
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -101,7 +110,7 @@ class DefaultRoomMembersModerationPresenterTest {
userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
) )
val selectedMember = aRoomMember(A_USER_ID_2, powerLevel = 100L) val selectedMember = aRoomMember(A_USER_ID_2, powerLevel = 100L)
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -125,7 +134,7 @@ class DefaultRoomMembersModerationPresenterTest {
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
) )
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -148,7 +157,7 @@ class DefaultRoomMembersModerationPresenterTest {
kickUserResult = { _, _ -> Result.success(Unit) }, kickUserResult = { _, _ -> Result.success(Unit) },
) )
val selectedMember = aVictor() val selectedMember = aVictor()
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) val presenter = createRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -176,7 +185,7 @@ class DefaultRoomMembersModerationPresenterTest {
banUserResult = { _, _ -> Result.success(Unit) }, banUserResult = { _, _ -> Result.success(Unit) },
) )
val selectedMember = aVictor() val selectedMember = aVictor()
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) val presenter = createRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -211,7 +220,7 @@ class DefaultRoomMembersModerationPresenterTest {
).apply { ).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(selectedMember))) givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(selectedMember)))
} }
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService) val presenter = createRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -237,7 +246,7 @@ class DefaultRoomMembersModerationPresenterTest {
canBanResult = { Result.success(true) }, canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.USER) }, userRoleResult = { Result.success(RoomMember.Role.USER) },
) )
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -261,7 +270,7 @@ class DefaultRoomMembersModerationPresenterTest {
unBanUserResult = { _, _ -> Result.failure(Throwable("Eek")) }, unBanUserResult = { _, _ -> Result.failure(Throwable("Eek")) },
userRoleResult = { Result.success(RoomMember.Role.USER) }, userRoleResult = { Result.success(RoomMember.Role.USER) },
) )
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) val presenter = createRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -301,12 +310,12 @@ class DefaultRoomMembersModerationPresenterTest {
} }
} }
private fun TestScope.createDefaultRoomMembersModerationPresenter( private fun TestScope.createRoomMembersModerationPresenter(
matrixRoom: FakeMatrixRoom = FakeMatrixRoom(), matrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
): DefaultRoomMembersModerationPresenter { ): RoomMembersModerationPresenter {
return DefaultRoomMembersModerationPresenter( return RoomMembersModerationPresenter(
room = matrixRoom, room = matrixRoom,
dispatchers = dispatchers, dispatchers = dispatchers,
analyticsService = analyticsService, analyticsService = analyticsService,

View file

@ -65,7 +65,6 @@ dependencies {
testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.libraries.eventformatter.test) testImplementation(projects.libraries.eventformatter.test)
testImplementation(projects.libraries.indicator.impl) testImplementation(projects.libraries.indicator.impl)
testImplementation(projects.libraries.fullscreenintent.test)
testImplementation(projects.libraries.permissions.noop) testImplementation(projects.libraries.permissions.noop)
testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)
@ -75,5 +74,4 @@ dependencies {
testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.logout.test) testImplementation(projects.features.logout.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
testImplementation(projects.features.leaveroom.test)
} }

View file

@ -10,6 +10,7 @@ package io.element.android.features.roomlist.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState 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.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -37,15 +38,3 @@ internal fun aRoomsContentState(
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16) internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
internal fun anEmptyContentState() = RoomListContentState.Empty 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,
)

View file

@ -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.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.leaveroom.api.LeaveRoomEvent 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.logout.api.direct.DirectLogoutPresenter import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource 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.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags 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.indicator.api.IndicatorService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -80,7 +80,7 @@ class RoomListPresenter @Inject constructor(
private val client: MatrixClient, private val client: MatrixClient,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val snackbarDispatcher: SnackbarDispatcher, private val snackbarDispatcher: SnackbarDispatcher,
private val leaveRoomPresenter: LeaveRoomPresenter, private val leaveRoomPresenter: Presenter<LeaveRoomState>,
private val roomListDataSource: RoomListDataSource, private val roomListDataSource: RoomListDataSource,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val indicatorService: IndicatorService, private val indicatorService: IndicatorService,
@ -89,9 +89,9 @@ class RoomListPresenter @Inject constructor(
private val sessionPreferencesStore: SessionPreferencesStore, private val sessionPreferencesStore: SessionPreferencesStore,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>, private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
private val fullScreenIntentPermissionsPresenter: FullScreenIntentPermissionsPresenter, private val fullScreenIntentPermissionsPresenter: Presenter<FullScreenIntentPermissionsState>,
private val notificationCleaner: NotificationCleaner, private val notificationCleaner: NotificationCleaner,
private val logoutPresenter: DirectLogoutPresenter, private val logoutPresenter: Presenter<DirectLogoutState>,
) : Presenter<RoomListState> { ) : Presenter<RoomListState> {
private val encryptionService: EncryptionService = client.encryptionService() private val encryptionService: EncryptionService = client.encryptionService()
private val syncService: SyncService = client.syncService() private val syncService: SyncService = client.syncService()

View file

@ -10,11 +10,11 @@ package io.element.android.features.roomlist.impl.components
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import io.element.android.features.roomlist.impl.R 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.atomic.molecules.DialogLikeBannerMolecule
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
@Composable @Composable
fun FullScreenIntentPermissionBanner(state: FullScreenIntentPermissionsState) { fun FullScreenIntentPermissionBanner(state: FullScreenIntentPermissionsState) {

View file

@ -7,7 +7,6 @@
package io.element.android.features.roomlist.impl package io.element.android.features.roomlist.impl
import androidx.compose.runtime.Composable
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test 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.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomEvent 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.fake.FakeLeaveRoomPresenter import io.element.android.features.leaveroom.api.aLeaveRoomState
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor 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.eventformatter.test.FakeRoomLastMessageFormatter
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService 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.indicator.impl.DefaultIndicatorService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.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.lambda.value
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceTimeBy
@ -102,7 +97,6 @@ class RoomListPresenterTest {
@Test @Test
fun `present - should start with no user and then load user with success`() = runTest { fun `present - should start with no user and then load user with success`() = runTest {
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
userDisplayName = null, userDisplayName = null,
userAvatarUrl = null, userAvatarUrl = null,
@ -110,7 +104,6 @@ class RoomListPresenterTest {
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL))) matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL)))
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = matrixClient, client = matrixClient,
coroutineScope = scope,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -122,13 +115,11 @@ class RoomListPresenterTest {
assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME) assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME)
assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL)
assertThat(withUserState.showAvatarIndicator).isTrue() assertThat(withUserState.showAvatarIndicator).isTrue()
scope.cancel()
} }
} }
@Test @Test
fun `present - show avatar indicator`() = runTest { fun `present - show avatar indicator`() = runTest {
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val encryptionService = FakeEncryptionService() val encryptionService = FakeEncryptionService()
val sessionVerificationService = FakeSessionVerificationService() val sessionVerificationService = FakeSessionVerificationService()
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
@ -137,7 +128,6 @@ class RoomListPresenterTest {
) )
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = matrixClient, client = matrixClient,
coroutineScope = scope
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -148,7 +138,6 @@ class RoomListPresenterTest {
encryptionService.emitBackupState(BackupState.ENABLED) encryptionService.emitBackupState(BackupState.ENABLED)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.showAvatarIndicator).isFalse() assertThat(finalState.showAvatarIndicator).isFalse()
scope.cancel()
} }
} }
@ -159,8 +148,7 @@ class RoomListPresenterTest {
userAvatarUrl = null, userAvatarUrl = null,
) )
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.failure(AN_EXCEPTION)) matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.failure(AN_EXCEPTION))
val scope = CoroutineScope(coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter(client = matrixClient)
val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -176,8 +164,7 @@ class RoomListPresenterTest {
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomListService = roomListService roomListService = roomListService
) )
val scope = CoroutineScope(coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter(client = matrixClient)
val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -202,13 +189,11 @@ class RoomListPresenterTest {
) )
) )
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
scope.cancel()
} }
} }
@Test @Test
fun `present - handle DismissRequestVerificationPrompt`() = runTest { fun `present - handle DismissRequestVerificationPrompt`() = runTest {
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
val roomListService = FakeRoomListService().apply { val roomListService = FakeRoomListService().apply {
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
} }
@ -218,7 +203,6 @@ class RoomListPresenterTest {
val syncService = FakeSyncService(MutableStateFlow(SyncState.Running)) val syncService = FakeSyncService(MutableStateFlow(SyncState.Running))
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = FakeMatrixClient(roomListService = roomListService, encryptionService = encryptionService, syncService = syncService), client = FakeMatrixClient(roomListService = roomListService, encryptionService = encryptionService, syncService = syncService),
coroutineScope = scope,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -230,7 +214,6 @@ class RoomListPresenterTest {
assertThat(eventWithContentAsRooms.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) assertThat(eventWithContentAsRooms.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
eventSink(RoomListEvents.DismissRequestVerificationPrompt) eventSink(RoomListEvents.DismissRequestVerificationPrompt)
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
scope.cancel()
} }
} }
@ -250,10 +233,8 @@ class RoomListPresenterTest {
}, },
syncService = FakeSyncService(MutableStateFlow(SyncState.Running)), syncService = FakeSyncService(MutableStateFlow(SyncState.Running)),
) )
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = matrixClient, client = matrixClient,
coroutineScope = scope,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -279,18 +260,16 @@ class RoomListPresenterTest {
nextState.eventSink(RoomListEvents.DismissBanner) nextState.eventSink(RoomListEvents.DismissBanner)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
scope.cancel()
} }
} }
@Test @Test
fun `present - show context menu`() = runTest { fun `present - show context menu`() = runTest {
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val client = FakeMatrixClient().apply { val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room) givenGetRoomResult(A_ROOM_ID, room)
} }
val presenter = createRoomListPresenter(client = client, coroutineScope = scope) val presenter = createRoomListPresenter(client = client)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -328,18 +307,16 @@ class RoomListPresenterTest {
) )
) )
} }
scope.cancel()
} }
} }
@Test @Test
fun `present - hide context menu`() = runTest { fun `present - hide context menu`() = runTest {
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val client = FakeMatrixClient().apply { val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room) givenGetRoomResult(A_ROOM_ID, room)
} }
val presenter = createRoomListPresenter(client = client, coroutineScope = scope) val presenter = createRoomListPresenter(client = client)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -364,23 +341,22 @@ class RoomListPresenterTest {
val hiddenState = awaitItem() val hiddenState = awaitItem()
assertThat(hiddenState.contextMenu).isEqualTo(RoomListState.ContextMenu.Hidden) assertThat(hiddenState.contextMenu).isEqualTo(RoomListState.ContextMenu.Hidden)
scope.cancel()
} }
} }
@Test @Test
fun `present - leave room calls into leave room presenter`() = runTest { fun `present - leave room calls into leave room presenter`() = runTest {
val leaveRoomPresenter = FakeLeaveRoomPresenter() val leaveRoomEventsRecorder = EventsRecorder<LeaveRoomEvent>()
val scope = CoroutineScope(coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter(
val presenter = createRoomListPresenter(leaveRoomPresenter = leaveRoomPresenter, coroutineScope = scope) leaveRoomState = aLeaveRoomState(eventSink = leaveRoomEventsRecorder),
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID)) initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID))
assertThat(leaveRoomPresenter.events).containsExactly(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID)) leaveRoomEventsRecorder.assertSingle(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
scope.cancel()
} }
} }
@ -392,9 +368,7 @@ class RoomListPresenterTest {
eventSink = eventRecorder eventSink = eventRecorder
) )
} }
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
coroutineScope = scope,
searchPresenter = searchPresenter, searchPresenter = searchPresenter,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@ -413,7 +387,6 @@ class RoomListPresenterTest {
RoomListSearchEvents.ToggleSearchVisibility RoomListSearchEvents.ToggleSearchVisibility
) )
) )
scope.cancel()
} }
} }
@ -428,8 +401,7 @@ class RoomListPresenterTest {
roomListService = roomListService, roomListService = roomListService,
notificationSettingsService = notificationSettingsService notificationSettingsService = notificationSettingsService
) )
val scope = CoroutineScope(coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter(client = matrixClient)
val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -443,13 +415,11 @@ class RoomListPresenterTest {
val room = updatedState.contentAsRooms().summaries.find { it.id == A_ROOM_ID.value } val room = updatedState.contentAsRooms().summaries.find { it.id == A_ROOM_ID.value }
assertThat(room?.userDefinedNotificationMode).isEqualTo(userDefinedMode) assertThat(room?.userDefinedNotificationMode).isEqualTo(userDefinedMode)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
scope.cancel()
} }
} }
@Test @Test
fun `present - when set is favorite event is emitted, then the action is called`() = runTest { 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 setIsFavoriteResult = lambdaRecorder { _: Boolean -> Result.success(Unit) }
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
setIsFavoriteResult = setIsFavoriteResult setIsFavoriteResult = setIsFavoriteResult
@ -458,7 +428,7 @@ class RoomListPresenterTest {
val client = FakeMatrixClient().apply { val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room) givenGetRoomResult(A_ROOM_ID, room)
} }
val presenter = createRoomListPresenter(client = client, coroutineScope = scope, analyticsService = analyticsService) val presenter = createRoomListPresenter(client = client, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -476,13 +446,11 @@ class RoomListPresenterTest {
Interaction(name = Interaction.Name.MobileRoomListRoomContextMenuFavouriteToggle) Interaction(name = Interaction.Name.MobileRoomListRoomContextMenuFavouriteToggle)
) )
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
scope.cancel()
} }
} }
@Test @Test
fun `present - when room service returns no room, then contentState is Empty`() = runTest { fun `present - when room service returns no room, then contentState is Empty`() = runTest {
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val roomListService = FakeRoomListService() val roomListService = FakeRoomListService()
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(0)) roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(0))
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
@ -490,13 +458,11 @@ class RoomListPresenterTest {
) )
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = matrixClient, client = matrixClient,
coroutineScope = scope,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java) assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java)
scope.cancel()
} }
} }
@ -513,14 +479,12 @@ class RoomListPresenterTest {
givenGetRoomResult(A_ROOM_ID_3, room3) givenGetRoomResult(A_ROOM_ID_3, room3)
} }
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val clearMessagesForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> } val clearMessagesForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
val notificationCleaner = FakeNotificationCleaner( val notificationCleaner = FakeNotificationCleaner(
clearMessagesForRoomLambda = clearMessagesForRoomLambda, clearMessagesForRoomLambda = clearMessagesForRoomLambda,
) )
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = matrixClient, client = matrixClient,
coroutineScope = scope,
sessionPreferencesStore = sessionPreferencesStore, sessionPreferencesStore = sessionPreferencesStore,
analyticsService = analyticsService, analyticsService = analyticsService,
notificationCleaner = notificationCleaner, notificationCleaner = notificationCleaner,
@ -557,7 +521,6 @@ class RoomListPresenterTest {
Interaction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle), Interaction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle),
) )
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
scope.cancel()
} }
} }
@ -568,7 +531,6 @@ class RoomListPresenterTest {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder) anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
} }
val roomListService = FakeRoomListService() val roomListService = FakeRoomListService()
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomListService = roomListService, roomListService = roomListService,
) )
@ -578,7 +540,6 @@ class RoomListPresenterTest {
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
roomListService.postAllRooms(listOf(roomSummary)) roomListService.postAllRooms(listOf(roomSummary))
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
coroutineScope = scope,
client = matrixClient, client = matrixClient,
acceptDeclineInvitePresenter = acceptDeclinePresenter acceptDeclineInvitePresenter = acceptDeclinePresenter
) )
@ -609,7 +570,6 @@ class RoomListPresenterTest {
fun `present - UpdateVisibleRange will cancel the previous subscription if called too soon`() = runTest { fun `present - UpdateVisibleRange will cancel the previous subscription if called too soon`() = runTest {
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> } val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda) val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomListService = roomListService, roomListService = roomListService,
) )
@ -619,7 +579,6 @@ class RoomListPresenterTest {
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
roomListService.postAllRooms(listOf(roomSummary)) roomListService.postAllRooms(listOf(roomSummary))
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
coroutineScope = scope,
client = matrixClient, client = matrixClient,
) )
presenter.test { presenter.test {
@ -640,7 +599,6 @@ class RoomListPresenterTest {
fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest { fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest {
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> } val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda) val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomListService = roomListService, roomListService = roomListService,
) )
@ -650,7 +608,6 @@ class RoomListPresenterTest {
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
roomListService.postAllRooms(listOf(roomSummary)) roomListService.postAllRooms(listOf(roomSummary))
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
coroutineScope = scope,
client = matrixClient, client = matrixClient,
) )
presenter.test { presenter.test {
@ -673,28 +630,23 @@ class RoomListPresenterTest {
client: MatrixClient = FakeMatrixClient(), client: MatrixClient = FakeMatrixClient(),
networkMonitor: NetworkMonitor = FakeNetworkMonitor(), networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(), leaveRoomState: LeaveRoomState = aLeaveRoomState(),
lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply { lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply {
givenFormat(A_FORMATTED_DATE) givenFormat(A_FORMATTED_DATE)
}, },
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(), roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
coroutineScope: CoroutineScope,
analyticsService: AnalyticsService = FakeAnalyticsService(), analyticsService: AnalyticsService = FakeAnalyticsService(),
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() }, filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() }, searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }, acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
logoutPresenter: DirectLogoutPresenter = object : DirectLogoutPresenter {
@Composable
override fun present() = aDirectLogoutState()
},
) = RoomListPresenter( ) = RoomListPresenter(
client = client, client = client,
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
snackbarDispatcher = snackbarDispatcher, snackbarDispatcher = snackbarDispatcher,
leaveRoomPresenter = leaveRoomPresenter, leaveRoomPresenter = { leaveRoomState },
roomListDataSource = RoomListDataSource( roomListDataSource = RoomListDataSource(
roomListService = client.roomListService, roomListService = client.roomListService,
roomListRoomSummaryFactory = RoomListRoomSummaryFactory( roomListRoomSummaryFactory = RoomListRoomSummaryFactory(
@ -703,7 +655,7 @@ class RoomListPresenterTest {
), ),
coroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers = testCoroutineDispatchers(),
notificationSettingsService = client.notificationSettingsService(), notificationSettingsService = client.notificationSettingsService(),
appScope = coroutineScope appScope = backgroundScope
), ),
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
indicatorService = DefaultIndicatorService( indicatorService = DefaultIndicatorService(
@ -715,8 +667,8 @@ class RoomListPresenterTest {
filtersPresenter = filtersPresenter, filtersPresenter = filtersPresenter,
analyticsService = analyticsService, analyticsService = analyticsService,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
fullScreenIntentPermissionsPresenter = FakeFullScreenIntentPermissionsPresenter(), fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
notificationCleaner = notificationCleaner, notificationCleaner = notificationCleaner,
logoutPresenter = logoutPresenter, logoutPresenter = { aDirectLogoutState() },
) )
} }

View file

@ -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<FullScreenIntentPermissionsState>

View file

@ -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,
)

View file

@ -27,7 +27,6 @@ dependencies {
implementation(projects.services.toolbox.api) implementation(projects.services.toolbox.api)
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
testImplementation(projects.libraries.fullscreenintent.test)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)

View file

@ -19,11 +19,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit 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.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn 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.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher
@ -33,14 +32,13 @@ import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
@ContributesBinding(AppScope::class) class FullScreenIntentPermissionsPresenter @Inject constructor(
class DefaultFullScreenIntentPermissionsPresenter @Inject constructor(
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
private val externalIntentLauncher: ExternalIntentLauncher, private val externalIntentLauncher: ExternalIntentLauncher,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val notificationManagerCompat: NotificationManagerCompat, private val notificationManagerCompat: NotificationManagerCompat,
preferencesDataStoreFactory: PreferenceDataStoreFactory, preferencesDataStoreFactory: PreferenceDataStoreFactory,
) : FullScreenIntentPermissionsPresenter { ) : Presenter<FullScreenIntentPermissionsState> {
companion object { companion object {
private const val PREF_KEY_FULL_SCREEN_INTENT_BANNER_DISMISSED = "PREF_KEY_FULL_SCREEN_INTENT_BANNER_DISMISSED" private const val PREF_KEY_FULL_SCREEN_INTENT_BANNER_DISMISSED = "PREF_KEY_FULL_SCREEN_INTENT_BANNER_DISMISSED"
} }

View file

@ -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<FullScreenIntentPermissionsState>
}

View file

@ -15,7 +15,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.meta.BuildMeta 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.matrix.test.core.aBuildMeta
import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory
import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher
@ -32,7 +32,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class DefaultFullScreenIntentPermissionsPresenterTest { class FullScreenIntentPermissionsPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@ -129,7 +129,7 @@ class DefaultFullScreenIntentPermissionsPresenterTest {
externalIntentLauncher: ExternalIntentLauncher = FakeExternalIntentLauncher(), externalIntentLauncher: ExternalIntentLauncher = FakeExternalIntentLauncher(),
buildMeta: BuildMeta = aBuildMeta(), buildMeta: BuildMeta = aBuildMeta(),
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true) notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true)
) = DefaultFullScreenIntentPermissionsPresenter( ) = FullScreenIntentPermissionsPresenter(
buildVersionSdkIntProvider = buildVersionSdkIntProvider, buildVersionSdkIntProvider = buildVersionSdkIntProvider,
externalIntentLauncher = externalIntentLauncher, externalIntentLauncher = externalIntentLauncher,
buildMeta = buildMeta, buildMeta = buildMeta,

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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.services.appnavstate.test.FakeAppNavigationStateService
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -39,7 +36,6 @@ class DefaultOnMissedCallNotificationHandlerTest {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun `addMissedCallNotification - should add missed call notification`() = runTest { fun `addMissedCallNotification - should add missed call notification`() = runTest {
val childScope = CoroutineScope(coroutineContext + SupervisorJob())
val dataFactory = FakeNotificationDataFactory( val dataFactory = FakeNotificationDataFactory(
messageEventToNotificationsResult = lambdaRecorder { _, _, _ -> emptyList() } messageEventToNotificationsResult = lambdaRecorder { _, _, _ -> emptyList() }
) )
@ -59,7 +55,7 @@ class DefaultOnMissedCallNotificationHandlerTest {
notificationDataFactory = dataFactory, notificationDataFactory = dataFactory,
), ),
appNavigationStateService = FakeAppNavigationStateService(), appNavigationStateService = FakeAppNavigationStateService(),
coroutineScope = childScope, coroutineScope = backgroundScope,
matrixClientProvider = FakeMatrixClientProvider(), matrixClientProvider = FakeMatrixClientProvider(),
imageLoaderHolder = FakeImageLoaderHolder(), imageLoaderHolder = FakeImageLoaderHolder(),
activeNotificationsProvider = FakeActiveNotificationsProvider(), activeNotificationsProvider = FakeActiveNotificationsProvider(),
@ -76,8 +72,5 @@ class DefaultOnMissedCallNotificationHandlerTest {
runCurrent() runCurrent()
dataFactory.messageEventToNotificationsResult.assertions().isCalledOnce() dataFactory.messageEventToNotificationsResult.assertions().isCalledOnce()
// Cancel the coroutine scope so the test can finish
childScope.cancel()
} }
} }

View file

@ -13,8 +13,8 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
import io.element.android.features.invite.impl.response.AcceptDeclineInviteView import io.element.android.features.invite.impl.response.AcceptDeclineInviteView
import io.element.android.features.leaveroom.impl.DefaultLeaveRoomPresenter import io.element.android.features.leaveroom.impl.LeaveRoomPresenter
import io.element.android.features.logout.impl.direct.DefaultDirectLogoutPresenter import io.element.android.features.logout.impl.direct.DirectLogoutPresenter
import io.element.android.features.networkmonitor.impl.DefaultNetworkMonitor import io.element.android.features.networkmonitor.impl.DefaultNetworkMonitor
import io.element.android.features.roomlist.impl.RoomListPresenter import io.element.android.features.roomlist.impl.RoomListPresenter
import io.element.android.features.roomlist.impl.RoomListView 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.ProfileChangeContentFormatter
import io.element.android.libraries.eventformatter.impl.RoomMembershipContentFormatter import io.element.android.libraries.eventformatter.impl.RoomMembershipContentFormatter
import io.element.android.libraries.eventformatter.impl.StateContentFormatter import io.element.android.libraries.eventformatter.impl.StateContentFormatter
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsPresenter import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.indicator.impl.DefaultIndicatorService import io.element.android.libraries.indicator.impl.DefaultIndicatorService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -87,7 +86,7 @@ class RoomListScreen(
client = matrixClient, client = matrixClient,
networkMonitor = DefaultNetworkMonitor(context, Singleton.appScope), networkMonitor = DefaultNetworkMonitor(context, Singleton.appScope),
snackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher = SnackbarDispatcher(),
leaveRoomPresenter = DefaultLeaveRoomPresenter(matrixClient, RoomMembershipObserver(), coroutineDispatchers), leaveRoomPresenter = LeaveRoomPresenter(matrixClient, RoomMembershipObserver(), coroutineDispatchers),
roomListDataSource = RoomListDataSource( roomListDataSource = RoomListDataSource(
roomListService = matrixClient.roomListService, roomListService = matrixClient.roomListService,
roomListRoomSummaryFactory = roomListRoomSummaryFactory, roomListRoomSummaryFactory = roomListRoomSummaryFactory,
@ -123,19 +122,9 @@ class RoomListScreen(
notificationCleaner = FakeNotificationCleaner(), notificationCleaner = FakeNotificationCleaner(),
), ),
analyticsService = NoopAnalyticsService(), analyticsService = NoopAnalyticsService(),
fullScreenIntentPermissionsPresenter = object : FullScreenIntentPermissionsPresenter { fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
@Composable
override fun present(): FullScreenIntentPermissionsState {
return FullScreenIntentPermissionsState(
permissionGranted = true,
shouldDisplayBanner = false,
dismissFullScreenIntentBanner = {},
openFullScreenIntentSettings = {}
)
}
},
notificationCleaner = FakeNotificationCleaner(), notificationCleaner = FakeNotificationCleaner(),
logoutPresenter = DefaultDirectLogoutPresenter(matrixClient, encryptionService), logoutPresenter = DirectLogoutPresenter(matrixClient, encryptionService),
) )
@Composable @Composable

View file

@ -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
}
}
}

View file

@ -7,16 +7,32 @@
package io.element.android.tests.konsist package io.element.android.tests.konsist
import com.google.common.truth.Truth.assertThat
import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutOverrideModifier 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.withFunction
import com.lemonappdev.konsist.api.ext.list.withReturnType 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.ext.list.withoutName
import com.lemonappdev.konsist.api.verify.assertFalse import com.lemonappdev.konsist.api.verify.assertFalse
import com.lemonappdev.konsist.api.verify.assertTrue import com.lemonappdev.konsist.api.verify.assertTrue
import org.junit.Ignore
import org.junit.Test import org.junit.Test
class KonsistTestTest { 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 @Test
fun `Classes name containing @Test must end with 'Test'`() { fun `Classes name containing @Test must end with 'Test'`() {
Konsist Konsist