Sync automatic call status with user status flag and server support

This commit is contained in:
ganfra
2026-08-06 11:09:49 +02:00
parent 93ca5374d6
commit c35a0a3864
6 changed files with 82 additions and 0 deletions
+1
View File
@@ -60,6 +60,7 @@ dependencies {
testCommonDependencies(libs)
testImplementation(projects.features.login.test)
testImplementation(projects.features.share.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oauth.test)
testImplementation(projects.libraries.permissions.test)
@@ -30,6 +30,8 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
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
@@ -65,6 +67,7 @@ class LoggedInPresenter(
private val buildMeta: BuildMeta,
private val networkMonitor: NetworkMonitor,
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
private val featureFlagService: FeatureFlagService,
permissionsPresenterFactory: PermissionsPresenter.Factory,
) : Presenter<LoggedInState> {
private val localNetworkPermissionsPresenter: PermissionsPresenter =
@@ -120,6 +123,16 @@ class LoggedInPresenter(
}.launchIn(this)
}
LaunchedEffect(Unit) {
// Keep automatic call status (m.call) in sync with the feature flag and homeserver support.
featureFlagService.isFeatureEnabledFlow(FeatureFlags.UserStatus)
.onEach { isUserStatusEnabled ->
val enabled = isUserStatusEnabled && matrixClient.isUserStatusSupported().getOrDefault(false)
matrixClient.enableAutomaticCallStatus(enabled)
}
.launchIn(this)
}
val networkConnectivity by networkMonitor.connectivity.collectAsState()
LaunchedEffect(networkConnectivity) {
if (networkConnectivity == NetworkStatus.Connected) {
@@ -17,6 +17,9 @@ import im.vector.app.features.analytics.plan.UserProperties
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
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
@@ -48,6 +51,7 @@ import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@@ -127,6 +131,7 @@ class LoggedInPresenterTest {
buildMeta = buildMeta,
networkMonitor = networkMonitor,
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
featureFlagService = FakeFeatureFlagService(),
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
).test {
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
@@ -346,6 +351,57 @@ class LoggedInPresenterTest {
}
}
@Test
fun `present - does not enable automatic call status when server does not support it`() = runTest {
val enableAutomaticCallStatusLambda = lambdaRecorder<Boolean, Unit> { }
val matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
enableAutomaticCallStatusLambda = enableAutomaticCallStatusLambda,
).apply {
isUserStatusSupportedResult = Result.success(false)
}
createLoggedInPresenter(
matrixClient = matrixClient,
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.UserStatus.key to true),
),
).test {
cancelAndConsumeRemainingEvents()
assert(enableAutomaticCallStatusLambda)
.isCalledOnce()
.with(value(false))
}
}
@Test
fun `present - reacts to user status flag being toggled at runtime`() = runTest {
val enableAutomaticCallStatusLambda = lambdaRecorder<Boolean, Unit> { }
val matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
enableAutomaticCallStatusLambda = enableAutomaticCallStatusLambda,
).apply {
isUserStatusSupportedResult = Result.success(true)
}
val featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.UserStatus.key to false),
)
createLoggedInPresenter(
matrixClient = matrixClient,
featureFlagService = featureFlagService,
).test {
awaitItem()
// Turning the flag on at runtime enables it.
featureFlagService.setFeatureEnabled(FeatureFlags.UserStatus, true)
cancelAndConsumeRemainingEvents()
assert(enableAutomaticCallStatusLambda)
.isCalledExactly(2)
.withSequence(
listOf(value(false)),
listOf(value(true)),
)
}
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(1)
return awaitItem()
@@ -362,6 +418,7 @@ class LoggedInPresenterTest {
),
buildMeta: BuildMeta = aBuildMeta(),
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
): LoggedInPresenter {
return LoggedInPresenter(
matrixClient = matrixClient,
@@ -373,6 +430,7 @@ class LoggedInPresenterTest {
buildMeta = buildMeta,
networkMonitor = networkMonitor,
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
featureFlagService = featureFlagService,
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
)
}
@@ -102,6 +102,9 @@ interface MatrixClient {
/** Whether the homeserver advertises support for user status (MSC4426). */
suspend fun isUserStatusSupported(): Result<Boolean>
/** Enable or disable automatically setting the user's status to "in a call" (m.call) while in a call. */
fun enableAutomaticCallStatus(enabled: Boolean)
suspend fun joinRoom(roomId: RoomId): Result<RoomInfo?>
suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomInfo?>
suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<RoomInfo?>
@@ -530,6 +530,10 @@ class RustMatrixClient(
}
}
override fun enableAutomaticCallStatus(enabled: Boolean) {
innerClient.enableAutomaticCallStatus(enabled)
}
override suspend fun joinRoom(roomId: RoomId): Result<RoomInfo?> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerClient.joinRoomById(roomId.value).destroy()
@@ -130,6 +130,7 @@ class FakeMatrixClient(
private val getMapStyleUrlResult: () -> Result<String?> = { lambdaError() },
private val getDatabaseSizesLambda: () -> Result<SdkStoreSizes> = { lambdaError() },
private val resetWellKnownConfigLambda: () -> Result<Unit> = { lambdaError() },
private val enableAutomaticCallStatusLambda: (Boolean) -> Unit = { },
override val contentScanner: ContentScanner? = null,
) : MatrixClient {
var setDisplayNameCalled: Boolean = false
@@ -291,6 +292,8 @@ class FakeMatrixClient(
override suspend fun isUserStatusSupported(): Result<Boolean> = isUserStatusSupportedResult
override fun enableAutomaticCallStatus(enabled: Boolean) = enableAutomaticCallStatusLambda(enabled)
override suspend fun joinRoom(roomId: RoomId): Result<RoomInfo?> = joinRoomLambda(roomId)
override suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomInfo?> {