Reduce FeatureFlags Knock effect on room creation and room edition form.

Closes #6701
This commit is contained in:
Benoit Marty 2026-05-12 12:23:16 +02:00
parent 4975f61eb8
commit 206c3b7dac
7 changed files with 10 additions and 59 deletions

View file

@ -33,9 +33,7 @@ dependencies {
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.featureflag.api)
testCommonDependencies(libs, true) testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test)
} }

View file

@ -15,8 +15,6 @@ import dev.zacsweers.metro.SingleIn
import io.element.android.features.knockrequests.api.KnockRequestPermissions import io.element.android.features.knockrequests.api.KnockRequestPermissions
import io.element.android.features.knockrequests.api.knockRequestPermissions import io.element.android.features.knockrequests.api.knockRequestPermissions
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsFlow import io.element.android.libraries.matrix.api.room.powerlevels.permissionsFlow
@ -25,14 +23,13 @@ import io.element.android.libraries.matrix.api.room.powerlevels.permissionsFlow
object KnockRequestsModule { object KnockRequestsModule {
@Provides @Provides
@SingleIn(RoomScope::class) @SingleIn(RoomScope::class)
fun knockRequestsService(room: JoinedRoom, featureFlagService: FeatureFlagService): KnockRequestsService { fun knockRequestsService(room: JoinedRoom): KnockRequestsService {
return KnockRequestsService( return KnockRequestsService(
knockRequestsFlow = room.knockRequestsFlow, knockRequestsFlow = room.knockRequestsFlow,
permissionsFlow = room.permissionsFlow(KnockRequestPermissions.DEFAULT) { perms -> permissionsFlow = room.permissionsFlow(KnockRequestPermissions.DEFAULT) { perms ->
perms.knockRequestPermissions() perms.knockRequestPermissions()
}, },
isKnockFeatureEnabledFlow = featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock), coroutineScope = room.roomCoroutineScope,
coroutineScope = room.roomCoroutineScope
) )
} }
} }

View file

@ -12,7 +12,6 @@ import io.element.android.features.knockrequests.api.KnockRequestPermissions
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
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.room.knock.KnockRequest import io.element.android.libraries.matrix.api.room.knock.KnockRequest
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -28,26 +27,20 @@ import kotlinx.coroutines.supervisorScope
class KnockRequestsService( class KnockRequestsService(
knockRequestsFlow: Flow<List<KnockRequest>>, knockRequestsFlow: Flow<List<KnockRequest>>,
permissionsFlow: Flow<KnockRequestPermissions>, permissionsFlow: Flow<KnockRequestPermissions>,
isKnockFeatureEnabledFlow: Flow<Boolean>,
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
) { ) {
// Keep track of the knock requests that have been handled, so we don't have to wait for sync to remove them. // Keep track of the knock requests that have been handled, so we don't have to wait for sync to remove them.
private val handledKnockRequestIds = MutableStateFlow<Set<EventId>>(emptySet()) private val handledKnockRequestIds = MutableStateFlow<Set<EventId>>(emptySet())
val knockRequestsFlow = combine( val knockRequestsFlow = combine(
isKnockFeatureEnabledFlow,
knockRequestsFlow, knockRequestsFlow,
handledKnockRequestIds, handledKnockRequestIds,
) { isKnockEnabled, knockRequests, handledKnockIds -> ) { knockRequests, handledKnockIds ->
if (!isKnockEnabled) { val presentableKnockRequests = knockRequests
AsyncData.Success(persistentListOf()) .filter { it.eventId !in handledKnockIds }
} else { .map { inner -> KnockRequestWrapper(inner) }
val presentableKnockRequests = knockRequests .toImmutableList()
.filter { it.eventId !in handledKnockIds } AsyncData.Success(presentableKnockRequests)
.map { inner -> KnockRequestWrapper(inner) }
.toImmutableList()
AsyncData.Success(presentableKnockRequests)
}
}.stateIn(coroutineScope, SharingStarted.Lazily, AsyncData.Loading()) }.stateIn(coroutineScope, SharingStarted.Lazily, AsyncData.Loading())
val permissionsFlow = permissionsFlow.stateIn( val permissionsFlow = permissionsFlow.stateIn(

View file

@ -28,18 +28,6 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class) class KnockRequestsBannerPresenterTest { @OptIn(ExperimentalCoroutinesApi::class) class KnockRequestsBannerPresenterTest {
@Test
fun `present - when feature is disabled then the banner should be hidden`() = runTest {
val knockRequests = flowOf(listOf(FakeKnockRequest()))
val presenter = createKnockRequestsBannerPresenter(isFeatureEnabled = false, knockRequestsFlow = knockRequests)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.isVisible).isFalse()
}
}
}
@Test @Test
fun `present - when empty knock request list then the banner should be hidden`() = runTest { fun `present - when empty knock request list then the banner should be hidden`() = runTest {
val knockRequests = flowOf(emptyList<KnockRequest>()) val knockRequests = flowOf(emptyList<KnockRequest>())
@ -229,12 +217,10 @@ import org.junit.Test
private fun TestScope.createKnockRequestsBannerPresenter( private fun TestScope.createKnockRequestsBannerPresenter(
knockRequestsFlow: Flow<List<KnockRequest>> = flowOf(emptyList()), knockRequestsFlow: Flow<List<KnockRequest>> = flowOf(emptyList()),
canAcceptKnockRequests: Boolean = true, canAcceptKnockRequests: Boolean = true,
isFeatureEnabled: Boolean = true,
): KnockRequestsBannerPresenter { ): KnockRequestsBannerPresenter {
val knockRequestsService = KnockRequestsService( val knockRequestsService = KnockRequestsService(
knockRequestsFlow = knockRequestsFlow, knockRequestsFlow = knockRequestsFlow,
coroutineScope = backgroundScope, coroutineScope = backgroundScope,
isKnockFeatureEnabledFlow = flowOf(isFeatureEnabled),
permissionsFlow = flowOf(KnockRequestPermissions(canAcceptKnockRequests, canAcceptKnockRequests, canAcceptKnockRequests)), permissionsFlow = flowOf(KnockRequestPermissions(canAcceptKnockRequests, canAcceptKnockRequests, canAcceptKnockRequests)),
) )
return KnockRequestsBannerPresenter( return KnockRequestsBannerPresenter(

View file

@ -298,7 +298,6 @@ internal fun TestScope.createKnockRequestsListPresenter(
val knockRequestsService = KnockRequestsService( val knockRequestsService = KnockRequestsService(
knockRequestsFlow = knockRequestsFlow, knockRequestsFlow = knockRequestsFlow,
coroutineScope = backgroundScope, coroutineScope = backgroundScope,
isKnockFeatureEnabledFlow = flowOf(true),
permissionsFlow = flowOf(KnockRequestPermissions(canAccept, canDecline, canBan)), permissionsFlow = flowOf(KnockRequestPermissions(canAccept, canDecline, canBan)),
) )
return KnockRequestsListPresenter(knockRequestsService = knockRequestsService) return KnockRequestsListPresenter(knockRequestsService = knockRequestsService)

View file

@ -35,8 +35,6 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
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.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
@ -61,7 +59,6 @@ import kotlinx.coroutines.launch
class RoomDetailsPresenter( class RoomDetailsPresenter(
private val client: MatrixClient, private val client: MatrixClient,
private val room: JoinedRoom, private val room: JoinedRoom,
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: Presenter<LeaveRoomState>, private val leaveRoomPresenter: Presenter<LeaveRoomState>,
@ -110,14 +107,11 @@ class RoomDetailsPresenter(
} }
} }
val isKnockRequestsEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
}.collectAsState(false)
val knockRequestsCount by produceState<Int?>(null) { val knockRequestsCount by produceState<Int?>(null) {
room.knockRequestsFlow.collect { value = it.size } room.knockRequestsFlow.collect { value = it.size }
} }
val canShowKnockRequests by remember { val canShowKnockRequests by remember {
derivedStateOf { isKnockRequestsEnabled && permissions.knockRequestsPermissions.hasAny && joinRule == JoinRule.Knock } derivedStateOf { permissions.knockRequestsPermissions.hasAny && joinRule == JoinRule.Knock }
} }
val canShowSecurityAndPrivacy by remember { val canShowSecurityAndPrivacy by remember {
derivedStateOf { !isDm && permissions.securityAndPrivacyPermissions.hasAny(isSpace = false, joinRule = joinRule) } derivedStateOf { !isDm && permissions.securityAndPrivacyPermissions.hasAny(isSpace = false, joinRule = joinRule) }

View file

@ -21,9 +21,6 @@ import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
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
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.core.UserId
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembersState
@ -80,11 +77,6 @@ class RoomDetailsPresenterTest {
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
analyticsService: AnalyticsService = FakeAnalyticsService(), analyticsService: AnalyticsService = FakeAnalyticsService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(
mapOf(
FeatureFlags.Knock.key to false,
)
),
encryptionService: FakeEncryptionService = FakeEncryptionService(), encryptionService: FakeEncryptionService = FakeEncryptionService(),
clipboardHelper: ClipboardHelper = FakeClipboardHelper(), clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore() appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore()
@ -106,7 +98,6 @@ class RoomDetailsPresenterTest {
return RoomDetailsPresenter( return RoomDetailsPresenter(
client = matrixClient, client = matrixClient,
room = room, room = room,
featureFlagService = featureFlagService,
notificationSettingsService = matrixClient.notificationSettingsService, notificationSettingsService = matrixClient.notificationSettingsService,
roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory, roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory,
leaveRoomPresenter = { leaveRoomState }, leaveRoomPresenter = { leaveRoomState },
@ -564,17 +555,11 @@ class RoomDetailsPresenterTest {
roomPermissions = roomPermissions(), roomPermissions = roomPermissions(),
joinRule = JoinRule.Knock, joinRule = JoinRule.Knock,
) )
val featureFlagService = FakeFeatureFlagService(
mapOf(FeatureFlags.Knock.key to false)
)
val presenter = createRoomDetailsPresenter( val presenter = createRoomDetailsPresenter(
room = room, room = room,
featureFlagService = featureFlagService,
) )
presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) {
skipItems(1) skipItems(1)
assertThat(awaitItem().canShowKnockRequests).isFalse()
featureFlagService.setFeatureEnabled(FeatureFlags.Knock, true)
assertThat(awaitItem().canShowKnockRequests).isTrue() assertThat(awaitItem().canShowKnockRequests).isTrue()
room.givenRoomInfo(aRoomInfo(joinRule = JoinRule.Invite)) room.givenRoomInfo(aRoomInfo(joinRule = JoinRule.Invite))
assertThat(awaitItem().canShowKnockRequests).isFalse() assertThat(awaitItem().canShowKnockRequests).isFalse()
@ -587,8 +572,7 @@ class RoomDetailsPresenterTest {
val room = aJoinedRoom( val room = aJoinedRoom(
roomPermissions = roomPermissions(), roomPermissions = roomPermissions(),
) )
val featureFlagService = FakeFeatureFlagService() val presenter = createRoomDetailsPresenter(room = room)
val presenter = createRoomDetailsPresenter(room = room, featureFlagService = featureFlagService)
presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) {
skipItems(1) skipItems(1)
with(awaitItem()) { with(awaitItem()) {