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:
commit
c7c870b5b4
73 changed files with 849 additions and 1029 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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() },
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,5 @@ open class AnalyticsSettingsStateProvider : PreviewParameterProvider<AnalyticsSe
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aAnalyticsSettingsState() = AnalyticsSettingsState(
|
fun aAnalyticsSettingsState() = AnalyticsSettingsState(
|
||||||
analyticsState = aAnalyticsPreferencesState(),
|
analyticsPreferencesState = aAnalyticsPreferencesState(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue