Merge pull request #4301 from element-hq/feature/bma/preloadAccountURL

Preload account urls
This commit is contained in:
Benoit Marty 2025-02-26 15:54:20 +01:00 committed by GitHub
commit d746f59352
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 137 additions and 106 deletions

View file

@ -26,6 +26,7 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
import io.element.android.libraries.matrix.api.sync.SyncService
@ -35,6 +36,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.RegistrationFailure
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -60,6 +62,7 @@ class LoggedInPresenter @Inject constructor(
pushService.ignoreRegistrationError(matrixClient.sessionId)
}.collectAsState(initial = false)
val pusherRegistrationState = remember<MutableState<AsyncData<Unit>>> { mutableStateOf(AsyncData.Uninitialized) }
LaunchedEffect(Unit) { preloadAccountManagementUrl() }
LaunchedEffect(Unit) {
sessionVerificationService.sessionVerifiedStatus
.onEach { sessionVerifiedStatus ->
@ -202,4 +205,9 @@ class LoggedInPresenter @Inject constructor(
analyticsService.capture(CryptoSessionStateChange(changeRecoveryState, changeVerificationState))
}
}
private fun CoroutineScope.preloadAccountManagementUrl() = launch {
matrixClient.getAccountManagementUrl(AccountManagementAction.Profile)
matrixClient.getAccountManagementUrl(AccountManagementAction.SessionsList)
}
}

View file

@ -5,12 +5,11 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.appnav.loggedin
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CryptoSessionStateChange
import im.vector.app.features.analytics.plan.UserProperties
@ -19,6 +18,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
import io.element.android.libraries.matrix.api.sync.SyncState
@ -45,8 +45,8 @@ import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@ -58,10 +58,7 @@ class LoggedInPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = createLoggedInPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
createLoggedInPresenter().test {
val initialState = awaitItem()
assertThat(initialState.showSyncSpinner).isFalse()
assertThat(initialState.pusherRegistrationState.isUninitialized()).isTrue()
@ -69,13 +66,32 @@ class LoggedInPresenterTest {
}
}
@Test
fun `present - ensure that account urls are preloaded`() = runTest {
val accountManagementUrlResult = lambdaRecorder<AccountManagementAction?, Result<String?>> { Result.success("aUrl") }
val matrixClient = FakeMatrixClient(
accountManagementUrlResult = accountManagementUrlResult,
)
createLoggedInPresenter(
matrixClient = matrixClient,
).test {
awaitItem()
advanceUntilIdle()
accountManagementUrlResult.assertions().isCalledExactly(2)
.withSequence(
listOf(value(AccountManagementAction.Profile)),
listOf(value(AccountManagementAction.SessionsList)),
)
}
}
@Test
fun `present - show sync spinner`() = runTest {
val roomListService = FakeRoomListService()
val presenter = createLoggedInPresenter(roomListService, SyncState.Running)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
createLoggedInPresenter(
syncState = SyncState.Running,
matrixClient = FakeMatrixClient(roomListService = roomListService),
).test {
val initialState = awaitItem()
assertThat(initialState.showSyncSpinner).isFalse()
roomListService.postSyncIndicator(RoomListService.SyncIndicator.Show)
@ -92,18 +108,18 @@ class LoggedInPresenterTest {
val verificationService = FakeSessionVerificationService()
val encryptionService = FakeEncryptionService()
val buildMeta = aBuildMeta()
val presenter = LoggedInPresenter(
matrixClient = FakeMatrixClient(roomListService = roomListService, encryptionService = encryptionService),
LoggedInPresenter(
matrixClient = FakeMatrixClient(
roomListService = roomListService,
encryptionService = encryptionService,
),
syncService = FakeSyncService(initialSyncState = SyncState.Running),
pushService = FakePushService(),
sessionVerificationService = verificationService,
analyticsService = analyticsService,
encryptionService = encryptionService,
buildMeta = buildMeta,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
).test {
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
verificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
@ -129,13 +145,10 @@ class LoggedInPresenterTest {
val verificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.NotVerified
)
val presenter = createLoggedInPresenter(
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = verificationService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
).test {
val finalState = awaitFirstItem()
assertThat(finalState.pusherRegistrationState.errorOrNull())
.isInstanceOf(PusherRegistrationFailure.AccountNotVerified::class.java)
@ -155,13 +168,13 @@ class LoggedInPresenterTest {
val pushService = createFakePushService(
registerWithLambda = lambda,
)
val presenter = createLoggedInPresenter(
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
),
).test {
val finalState = awaitFirstItem()
assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue()
lambda.assertions()
@ -188,13 +201,13 @@ class LoggedInPresenterTest {
val pushService = createFakePushService(
registerWithLambda = lambda,
)
val presenter = createLoggedInPresenter(
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
),
).test {
val finalState = awaitFirstItem()
assertThat(finalState.pusherRegistrationState.isFailure()).isTrue()
lambda.assertions()
@ -233,13 +246,13 @@ class LoggedInPresenterTest {
currentPushProvider = { pushProvider },
registerWithLambda = lambda,
)
val presenter = createLoggedInPresenter(
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
),
).test {
val finalState = awaitFirstItem()
assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue()
lambda.assertions()
@ -277,13 +290,13 @@ class LoggedInPresenterTest {
currentPushProvider = { pushProvider },
registerWithLambda = lambda,
)
val presenter = createLoggedInPresenter(
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
),
).test {
val finalState = awaitFirstItem()
assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue()
lambda.assertions()
@ -317,13 +330,10 @@ class LoggedInPresenterTest {
currentPushProvider = { pushProvider },
registerWithLambda = lambda,
)
val presenter = createLoggedInPresenter(
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
).test {
val finalState = awaitFirstItem()
assertThat(finalState.pusherRegistrationState.errorOrNull())
.isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java)
@ -345,13 +355,10 @@ class LoggedInPresenterTest {
registerWithLambda = lambda,
setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda,
)
val presenter = createLoggedInPresenter(
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
).test {
val finalState = awaitFirstItem()
assertThat(finalState.pusherRegistrationState.errorOrNull())
.isInstanceOf(PusherRegistrationFailure.NoProvidersAvailable::class.java)
@ -394,13 +401,10 @@ class LoggedInPresenterTest {
registerWithLambda = lambda,
selectPushProviderLambda = selectPushProviderLambda,
)
val presenter = createLoggedInPresenter(
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
).test {
val finalState = awaitFirstItem()
assertThat(finalState.pusherRegistrationState.errorOrNull())
.isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java)
@ -445,13 +449,13 @@ class LoggedInPresenterTest {
pushProvider1 = pushProvider1,
registerWithLambda = lambda,
)
val presenter = createLoggedInPresenter(
createLoggedInPresenter(
pushService = pushService,
sessionVerificationService = sessionVerificationService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
),
).test {
val finalState = awaitFirstItem()
assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue()
lambda.assertions().isCalledOnce()
@ -505,10 +509,9 @@ class LoggedInPresenterTest {
currentSlidingSyncVersionLambda = { Result.success(SlidingSyncVersion.Proxy) },
availableSlidingSyncVersionsLambda = { Result.success(listOf(SlidingSyncVersion.Native)) },
)
val presenter = createLoggedInPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
createLoggedInPresenter(
matrixClient = matrixClient,
).test {
val initialState = awaitItem()
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
@ -525,13 +528,14 @@ class LoggedInPresenterTest {
assertThat(userInitiated).isTrue()
assertThat(ignoreSdkError).isTrue()
}
val matrixClient = FakeMatrixClient().apply {
val matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
).apply {
this.logoutLambda = logoutLambda
}
val presenter = createLoggedInPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
createLoggedInPresenter(
matrixClient = matrixClient,
).test {
val initialState = awaitItem()
initialState.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync)
@ -547,14 +551,15 @@ class LoggedInPresenterTest {
return awaitItem()
}
private fun TestScope.createLoggedInPresenter(
roomListService: RoomListService = FakeRoomListService(),
private fun createLoggedInPresenter(
syncState: SyncState = SyncState.Running,
analyticsService: AnalyticsService = FakeAnalyticsService(),
sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
encryptionService: EncryptionService = FakeEncryptionService(),
pushService: PushService = FakePushService(),
matrixClient: MatrixClient = FakeMatrixClient(roomListService = roomListService),
matrixClient: MatrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
),
buildMeta: BuildMeta = aBuildMeta(),
): LoggedInPresenter {
return LoggedInPresenter(

View file

@ -5,12 +5,11 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.preferences.impl.root
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
@ -18,6 +17,7 @@ import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.indicator.impl.DefaultIndicatorService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_USER_NAME
@ -27,6 +27,10 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ -37,11 +41,16 @@ class PreferencesRootPresenterTest {
@Test
fun `present - initial state`() = runTest {
val matrixClient = FakeMatrixClient(canDeactivateAccountResult = { true })
val presenter = createPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val accountManagementUrlResult = lambdaRecorder<AccountManagementAction?, Result<String?>> { action ->
Result.success("$action url")
}
val matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = accountManagementUrlResult,
)
createPresenter(
matrixClient = matrixClient,
).test {
val initialState = awaitItem()
assertThat(initialState.myUser).isEqualTo(
MatrixUser(
@ -71,19 +80,26 @@ class PreferencesRootPresenterTest {
assertThat(loadedState.canDeactivateAccount).isTrue()
assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState())
assertThat(loadedState.snackbarMessage).isNull()
skipItems(1)
val finalState = awaitItem()
accountManagementUrlResult.assertions().isCalledExactly(2)
.withSequence(
listOf(value(AccountManagementAction.Profile)),
listOf(value(AccountManagementAction.SessionsList)),
)
assertThat(finalState.accountManagementUrl).isEqualTo("Profile url")
assertThat(finalState.devicesManagementUrl).isEqualTo("SessionsList url")
}
}
@Test
fun `present - can deactivate account is false if the Matrix client say so`() = runTest {
val presenter = createPresenter(
createPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { false }
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
canDeactivateAccountResult = { false },
accountManagementUrlResult = { Result.success(null) },
),
).test {
val loadedState = awaitFirstItem()
assertThat(loadedState.canDeactivateAccount).isFalse()
}
@ -91,12 +107,13 @@ class PreferencesRootPresenterTest {
@Test
fun `present - developer settings is hidden by default in release builds`() = runTest {
val presenter = createPresenter(
createPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE))
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
).test {
val loadedState = awaitFirstItem()
assertThat(loadedState.showDeveloperSettings).isFalse()
}
@ -104,12 +121,13 @@ class PreferencesRootPresenterTest {
@Test
fun `present - developer settings can be enabled in release builds`() = runTest {
val presenter = createPresenter(
createPresenter(
matrixClient = FakeMatrixClient(
canDeactivateAccountResult = { true },
accountManagementUrlResult = { Result.success(null) },
),
showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE))
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
).test {
val loadedState = awaitFirstItem()
repeat(times = ShowDeveloperSettingsProvider.DEVELOPER_SETTINGS_COUNTER) {
assertThat(loadedState.showDeveloperSettings).isFalse()
@ -125,7 +143,7 @@ class PreferencesRootPresenterTest {
}
private fun createPresenter(
matrixClient: FakeMatrixClient = FakeMatrixClient(canDeactivateAccountResult = { true }),
matrixClient: FakeMatrixClient = FakeMatrixClient(),
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)),
) = PreferencesRootPresenter(

View file

@ -72,11 +72,11 @@ class FakeMatrixClient(
private val syncService: FakeSyncService = FakeSyncService(),
private val encryptionService: FakeEncryptionService = FakeEncryptionService(),
private val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(),
private val accountManagementUrlString: Result<String?> = Result.success(null),
private val accountManagementUrlResult: (AccountManagementAction?) -> Result<String?> = { lambdaError() },
private val resolveRoomAliasResult: (RoomAlias) -> Result<Optional<ResolvedRoomAlias>> = {
Result.success(
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
)
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
)
},
private val getRoomPreviewResult: (RoomIdOrAlias, List<String>) -> Result<RoomPreview> = { _, _ -> Result.failure(AN_EXCEPTION) },
private val clearCacheLambda: () -> Unit = { lambdaError() },
@ -190,8 +190,8 @@ class FakeMatrixClient(
return Result.success(result)
}
override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> {
return accountManagementUrlString
override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> = simulateLongTask {
accountManagementUrlResult(action)
}
override suspend fun uploadMedia(