diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt index cded6e38de..a4a13986e8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt @@ -15,6 +15,8 @@ import androidx.compose.runtime.rememberCoroutineScope import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -31,7 +33,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber @@ -40,6 +41,7 @@ import javax.inject.Inject class IdentityChangeStatePresenter @Inject constructor( private val room: MatrixRoom, private val encryptionService: EncryptionService, + private val featureFlagService: FeatureFlagService, ) : Presenter { @Composable override fun present(): IdentityChangeState { @@ -62,14 +64,18 @@ class IdentityChangeStatePresenter @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) private fun ProduceStateScope>.observeRoomMemberIdentityStateChange() { - room.syncUpdateFlow + featureFlagService.isFeatureEnabledFlow(FeatureFlags.IdentityPinningViolationNotifications) + .filter { it } + .flatMapLatest { + room.syncUpdateFlow + } .filter { // Room cannot become unencrypted, so we can just apply a filter here. room.isEncrypted } .distinctUntilChanged() .flatMapLatest { - combine(room.identityStateChangesFlow, room.membersStateFlow,) { identityStateChanges, membersState -> + combine(room.identityStateChangesFlow, room.membersStateFlow) { identityStateChanges, membersState -> identityStateChanges.map { identityStateChange -> val member = membersState.roomMembers() ?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt index afc21efd97..4ff0c8f340 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt @@ -9,6 +9,9 @@ package io.element.android.features.messages.impl.crypto.identity import com.google.common.truth.Truth.assertThat import io.element.android.libraries.designsystem.components.avatar.AvatarSize +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.core.UserId import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.identity.IdentityState @@ -65,6 +68,43 @@ class IdentityChangeStatePresenterTest { } } + @Test + fun `present - when the room emits identity change, but the feature is disabled, the presenter emits new state`() = runTest { + val room = FakeMatrixRoom( + isEncrypted = true, + ) + val featureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.IdentityPinningViolationNotifications.key to false, + ) + ) + val presenter = createIdentityChangeStatePresenter( + room = room, + featureFlagService = featureFlagService, + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.roomMemberIdentityStateChanges).isEmpty() + room.emitIdentityStateChanges( + listOf( + IdentityStateChange( + userId = A_USER_ID_2, + identityState = IdentityState.PinViolation, + ), + ) + ) + // No item emitted. + expectNoEvents() + // Enable the feature + featureFlagService.setFeatureEnabled(FeatureFlags.IdentityPinningViolationNotifications, true) + val finalItem = awaitItem() + assertThat(finalItem.roomMemberIdentityStateChanges).hasSize(1) + val value = finalItem.roomMemberIdentityStateChanges.first() + assertThat(value.identityRoomMember.userId).isEqualTo(A_USER_ID_2) + assertThat(value.identityState).isEqualTo(IdentityState.PinViolation) + } + } + @Test fun `present - when the clear room emits identity change, the presenter does not emits new state`() = runTest { val room = FakeMatrixRoom(isEncrypted = false) @@ -147,10 +187,16 @@ class IdentityChangeStatePresenterTest { private fun createIdentityChangeStatePresenter( room: MatrixRoom = FakeMatrixRoom(), encryptionService: EncryptionService = FakeEncryptionService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.IdentityPinningViolationNotifications.key to true, + ) + ), ): IdentityChangeStatePresenter { return IdentityChangeStatePresenter( room = room, encryptionService = encryptionService, + featureFlagService = featureFlagService, ) } } diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index a26a499e5a..9f5b985435 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -125,4 +125,17 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), + IdentityPinningViolationNotifications( + key = "feature.identityPinningViolationNotifications", + title = "Identity pinning violation notifications", + description = null, + defaultValue = { buildMeta -> + when (buildMeta.buildType) { + // Do not enable this feature in release builds + BuildType.RELEASE -> false + else -> true + } + }, + isFinished = false, + ), }