Introduce SecurityBannerState to replace Boolean set.

Also get the sessionVerificationService from the matrixClient, instead of injecting it separately.
This commit is contained in:
Benoit Marty 2024-02-20 10:23:48 +01:00 committed by Benoit Marty
parent 7ec876bc8a
commit 4bc977d8dc
6 changed files with 55 additions and 59 deletions

View file

@ -73,7 +73,6 @@ private const val EXTENDED_RANGE_SIZE = 40
class RoomListPresenter @Inject constructor( class RoomListPresenter @Inject constructor(
private val client: MatrixClient, private val client: MatrixClient,
private val sessionVerificationService: SessionVerificationService,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val snackbarDispatcher: SnackbarDispatcher, private val snackbarDispatcher: SnackbarDispatcher,
private val inviteStateDataSource: InviteStateDataSource, private val inviteStateDataSource: InviteStateDataSource,
@ -87,6 +86,7 @@ class RoomListPresenter @Inject constructor(
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
) : Presenter<RoomListState> { ) : Presenter<RoomListState> {
private val encryptionService: EncryptionService = client.encryptionService() private val encryptionService: EncryptionService = client.encryptionService()
private val sessionVerificationService: SessionVerificationService = client.sessionVerificationService()
@Composable @Composable
override fun present(): RoomListState { override fun present(): RoomListState {
@ -108,22 +108,25 @@ class RoomListPresenter @Inject constructor(
val isMigrating = migrationScreenPresenter.present().isMigrating val isMigrating = migrationScreenPresenter.present().isMigrating
// Session verification status (unknown, not verified, verified) var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false) val displayVerificationPrompt by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) }
// We combine both values to only display the prompt if the session is not verified and it wasn't dismissed
val displayVerificationPrompt by remember {
derivedStateOf { canVerifySession && !verificationPromptDismissed }
}
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
.collectAsState(initial = null) .collectAsState(initial = null)
var recoveryKeyPromptDismissed by rememberSaveable { mutableStateOf(false) }
val displayRecoveryKeyPrompt by remember { val displayRecoveryKeyPrompt by remember {
derivedStateOf { derivedStateOf {
secureStorageFlag == true && secureStorageFlag == true &&
recoveryState == RecoveryState.INCOMPLETE && recoveryState == RecoveryState.INCOMPLETE
!recoveryKeyPromptDismissed }
}
val securityBannerState by remember {
derivedStateOf {
when {
securityBannerDismissed -> SecurityBannerState.None
displayVerificationPrompt -> SecurityBannerState.SessionVerification
displayRecoveryKeyPrompt -> SecurityBannerState.RecoveryKeyConfirmation
else -> SecurityBannerState.None
}
} }
} }
@ -135,8 +138,8 @@ class RoomListPresenter @Inject constructor(
fun handleEvents(event: RoomListEvents) { fun handleEvents(event: RoomListEvents) {
when (event) { when (event) {
is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range) is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range)
RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
RoomListEvents.DismissRecoveryKeyPrompt -> recoveryKeyPromptDismissed = true RoomListEvents.DismissRecoveryKeyPrompt -> securityBannerDismissed = true
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility) RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
is RoomListEvents.ShowContextMenu -> { is RoomListEvents.ShowContextMenu -> {
coroutineScope.showContextMenu(event, contextMenu) coroutineScope.showContextMenu(event, contextMenu)
@ -157,8 +160,7 @@ class RoomListPresenter @Inject constructor(
matrixUser = matrixUser.value, matrixUser = matrixUser.value,
showAvatarIndicator = showAvatarIndicator, showAvatarIndicator = showAvatarIndicator,
roomList = roomList, roomList = roomList,
displayVerificationPrompt = displayVerificationPrompt, securityBannerState = securityBannerState,
displayRecoveryKeyPrompt = displayRecoveryKeyPrompt,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
invitesState = inviteStateDataSource.inviteState(), invitesState = inviteStateDataSource.inviteState(),

View file

@ -31,8 +31,7 @@ data class RoomListState(
val matrixUser: MatrixUser?, val matrixUser: MatrixUser?,
val showAvatarIndicator: Boolean, val showAvatarIndicator: Boolean,
val roomList: AsyncData<ImmutableList<RoomListRoomSummary>>, val roomList: AsyncData<ImmutableList<RoomListRoomSummary>>,
val displayVerificationPrompt: Boolean, val securityBannerState: SecurityBannerState,
val displayRecoveryKeyPrompt: Boolean,
val hasNetworkConnection: Boolean, val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?, val snackbarMessage: SnackbarMessage?,
val invitesState: InvitesState, val invitesState: InvitesState,
@ -62,3 +61,9 @@ enum class InvitesState {
SeenInvites, SeenInvites,
NewInvites, NewInvites,
} }
enum class SecurityBannerState {
None,
SessionVerification,
RecoveryKeyConfirmation,
}

View file

@ -37,14 +37,14 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
override val values: Sequence<RoomListState> override val values: Sequence<RoomListState>
get() = sequenceOf( get() = sequenceOf(
aRoomListState(), aRoomListState(),
aRoomListState().copy(displayVerificationPrompt = true), aRoomListState().copy(securityBannerState = SecurityBannerState.SessionVerification),
aRoomListState().copy(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)), aRoomListState().copy(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
aRoomListState().copy(hasNetworkConnection = false), aRoomListState().copy(hasNetworkConnection = false),
aRoomListState().copy(invitesState = InvitesState.SeenInvites), aRoomListState().copy(invitesState = InvitesState.SeenInvites),
aRoomListState().copy(invitesState = InvitesState.NewInvites), aRoomListState().copy(invitesState = InvitesState.NewInvites),
aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")), aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")),
aRoomListState().copy(contextMenu = aContextMenuShown(isFavorite = true)), aRoomListState().copy(contextMenu = aContextMenuShown(isFavorite = true)),
aRoomListState().copy(displayRecoveryKeyPrompt = true), aRoomListState().copy(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())), aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())),
aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())), aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
aRoomListState().copy(matrixUser = null, displayMigrationStatus = true), aRoomListState().copy(matrixUser = null, displayMigrationStatus = true),
@ -58,8 +58,7 @@ internal fun aRoomListState() = RoomListState(
roomList = AsyncData.Success(aRoomListRoomSummaryList()), roomList = AsyncData.Success(aRoomListRoomSummaryList()),
hasNetworkConnection = true, hasNetworkConnection = true,
snackbarMessage = null, snackbarMessage = null,
displayVerificationPrompt = false, securityBannerState = SecurityBannerState.None,
displayRecoveryKeyPrompt = false,
invitesState = InvitesState.NoInvites, invitesState = InvitesState.NoInvites,
contextMenu = RoomListState.ContextMenu.Hidden, contextMenu = RoomListState.ContextMenu.Hidden,
leaveRoomState = aLeaveRoomState(), leaveRoomState = aLeaveRoomState(),

View file

@ -225,23 +225,25 @@ private fun RoomListContent(
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80 // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80
contentPadding = PaddingValues(bottom = 80.dp) contentPadding = PaddingValues(bottom = 80.dp)
) { ) {
when { if (state.displayEmptyState.not()) {
state.displayEmptyState -> Unit when (state.securityBannerState) {
state.displayVerificationPrompt -> { SecurityBannerState.SessionVerification -> {
item { item {
RequestVerificationHeader( RequestVerificationHeader(
onVerifyClicked = onVerifyClicked, onVerifyClicked = onVerifyClicked,
onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) } onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) }
) )
}
} }
} SecurityBannerState.RecoveryKeyConfirmation -> {
state.displayRecoveryKeyPrompt -> { item {
item { ConfirmRecoveryKeyBanner(
ConfirmRecoveryKeyBanner( onContinueClicked = onOpenSettings,
onContinueClicked = onOpenSettings, onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) } )
) }
} }
SecurityBannerState.None -> Unit
} }
} }

View file

@ -55,8 +55,6 @@ import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
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_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -114,13 +112,13 @@ class RoomListPresenterTests {
fun `present - show avatar indicator`() = runTest { fun `present - show avatar indicator`() = runTest {
val scope = CoroutineScope(coroutineContext + SupervisorJob()) val scope = CoroutineScope(coroutineContext + SupervisorJob())
val encryptionService = FakeEncryptionService() val encryptionService = FakeEncryptionService()
val sessionVerificationService = FakeSessionVerificationService()
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
encryptionService = encryptionService, encryptionService = encryptionService,
sessionVerificationService = sessionVerificationService,
) )
val sessionVerificationService = FakeSessionVerificationService()
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = matrixClient, client = matrixClient,
sessionVerificationService = sessionVerificationService,
coroutineScope = scope coroutineScope = scope
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@ -239,27 +237,17 @@ class RoomListPresenterTests {
@Test @Test
fun `present - handle DismissRequestVerificationPrompt`() = runTest { fun `present - handle DismissRequestVerificationPrompt`() = runTest {
val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(
roomListService = roomListService,
)
val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = matrixClient,
sessionVerificationService = FakeSessionVerificationService().apply {
givenIsReady(true)
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
},
coroutineScope = scope, coroutineScope = scope,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val eventSink = awaitItem().eventSink val eventSink = awaitItem().eventSink
assertThat(awaitItem().displayVerificationPrompt).isTrue() assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.SessionVerification)
eventSink(RoomListEvents.DismissRequestVerificationPrompt) eventSink(RoomListEvents.DismissRequestVerificationPrompt)
assertThat(awaitItem().displayVerificationPrompt).isFalse() assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.None)
scope.cancel() scope.cancel()
} }
} }
@ -269,6 +257,9 @@ class RoomListPresenterTests {
val encryptionService = FakeEncryptionService() val encryptionService = FakeEncryptionService()
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
encryptionService = encryptionService, encryptionService = encryptionService,
sessionVerificationService = FakeSessionVerificationService().apply {
givenCanVerifySession(false)
},
) )
val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
@ -280,13 +271,13 @@ class RoomListPresenterTests {
}.test { }.test {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.displayRecoveryKeyPrompt).isFalse() assertThat(initialState.securityBannerState).isEqualTo(SecurityBannerState.None)
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
val nextState = awaitItem() val nextState = awaitItem()
assertThat(nextState.displayRecoveryKeyPrompt).isTrue() assertThat(nextState.securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.displayRecoveryKeyPrompt).isFalse() assertThat(finalState.securityBannerState).isEqualTo(SecurityBannerState.None)
scope.cancel() scope.cancel()
} }
} }
@ -579,7 +570,6 @@ class RoomListPresenterTests {
private fun TestScope.createRoomListPresenter( private fun TestScope.createRoomListPresenter(
client: MatrixClient = FakeMatrixClient(), client: MatrixClient = FakeMatrixClient(),
sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
networkMonitor: NetworkMonitor = FakeNetworkMonitor(), networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
inviteStateDataSource: InviteStateDataSource = FakeInviteDataSource(), inviteStateDataSource: InviteStateDataSource = FakeInviteDataSource(),
@ -599,7 +589,6 @@ class RoomListPresenterTests {
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() }, searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
) = RoomListPresenter( ) = RoomListPresenter(
client = client, client = client,
sessionVerificationService = sessionVerificationService,
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
snackbarDispatcher = snackbarDispatcher, snackbarDispatcher = snackbarDispatcher,
inviteStateDataSource = inviteStateDataSource, inviteStateDataSource = inviteStateDataSource,
@ -616,7 +605,7 @@ class RoomListPresenterTests {
), ),
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
indicatorService = DefaultIndicatorService( indicatorService = DefaultIndicatorService(
sessionVerificationService = sessionVerificationService, sessionVerificationService = client.sessionVerificationService(),
encryptionService = client.encryptionService(), encryptionService = client.encryptionService(),
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
), ),

View file

@ -90,7 +90,6 @@ class RoomListScreen(
) )
private val presenter = RoomListPresenter( private val presenter = RoomListPresenter(
client = matrixClient, client = matrixClient,
sessionVerificationService = sessionVerificationService,
networkMonitor = NetworkMonitorImpl(context, Singleton.appScope), networkMonitor = NetworkMonitorImpl(context, Singleton.appScope),
snackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher = SnackbarDispatcher(),
inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers), inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers),