Add banner for optional migration to simplified sliding sync (#3429)

* Add banner for optional migration to native sliding sync

- Add `MatrixClient.isNativeSlidingSyncSupported()` and `MatrixClient.isUsingNativeSlidingSync` to check whether the home server supports native sliding sync and we're already using it.
- Add `NativeSlidingSyncMigrationBanner` composable to the `RoomList` screen when the home server supports native sliding sync but the current session is not using it.
- Add an extra logout successful action to the logout flow, create `EnableNativeSlidingSyncUseCase` so it can be used there.

* Update screenshots

* Make sure the sliding sync migration banner has lower priority than the encryption setup ones

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2024-09-09 18:13:19 +02:00 committed by GitHub
parent 7549d5f475
commit 67e262fdc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 283 additions and 12 deletions

View file

@ -16,6 +16,7 @@ interface LogoutEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder {
fun onSuccessfulLogoutPendingAction(action: () -> Unit): NodeBuilder
fun callback(callback: Callback): NodeBuilder
fun build(): Node
}

View file

@ -27,6 +27,15 @@ class DefaultLogoutEntryPoint @Inject constructor() : LogoutEntryPoint {
return this
}
override fun onSuccessfulLogoutPendingAction(action: () -> Unit): LogoutEntryPoint.NodeBuilder {
plugins += object : LogoutNode.SuccessfulLogoutPendingAction, Plugin {
override fun onSuccessfulLogoutPendingAction() {
action()
}
}
return this
}
override fun build(): Node {
return parentNode.createNode<LogoutNode>(buildContext, plugins)
}

View file

@ -33,6 +33,12 @@ class LogoutNode @AssistedInject constructor(
plugins<LogoutEntryPoint.Callback>().forEach { it.onChangeRecoveryKeyClick() }
}
interface SuccessfulLogoutPendingAction : Plugin {
fun onSuccessfulLogoutPendingAction()
}
private val customOnSuccessfulLogoutPendingAction = plugins<SuccessfulLogoutPendingAction>().firstOrNull()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@ -41,7 +47,10 @@ class LogoutNode @AssistedInject constructor(
LogoutView(
state = state,
onChangeRecoveryKeyClick = ::onChangeRecoveryKeyClick,
onSuccessLogout = { onSuccessLogout(activity, isDark, it) },
onSuccessLogout = {
customOnSuccessfulLogoutPendingAction?.onSuccessfulLogoutPendingAction()
onSuccessLogout(activity, isDark, it)
},
onBackClick = ::navigateUp,
modifier = modifier,
)

View file

@ -29,5 +29,6 @@ interface RoomListEntryPoint : FeatureEntryPoint {
fun onRoomSettingsClick(roomId: RoomId)
fun onReportBugClick()
fun onRoomDirectorySearchClick()
fun onLogoutForNativeSlidingSyncMigrationNeeded()
}
}

View file

@ -49,6 +49,7 @@ dependencies {
implementation(projects.libraries.push.api)
implementation(projects.features.invite.api)
implementation(projects.features.networkmonitor.api)
implementation(projects.features.logout.api)
implementation(projects.features.leaveroom.api)
implementation(projects.services.analytics.api)
implementation(libs.androidx.datastore.preferences)
@ -75,6 +76,7 @@ dependencies {
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.logout.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.features.leaveroom.test)
}

View file

@ -20,6 +20,7 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
aRoomsContentState(summaries = persistentListOf()),
aSkeletonContentState(),
anEmptyContentState(),
aRoomsContentState(securityBannerState = SecurityBannerState.NeedsNativeSlidingSyncMigration),
)
}

View file

@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
sealed interface RoomListEvents {
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
data object DismissRequestVerificationPrompt : RoomListEvents
data object DismissRecoveryKeyPrompt : RoomListEvents
data object DismissBanner : RoomListEvents
data object ToggleSearchResults : RoomListEvents
data class AcceptInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
data class DeclineInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents

View file

@ -21,11 +21,14 @@ import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.invite.api.response.AcceptDeclineInviteView
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(SessionScope::class)
@ -36,6 +39,8 @@ class RoomListNode @AssistedInject constructor(
private val inviteFriendsUseCase: InviteFriendsUseCase,
private val analyticsService: AnalyticsService,
private val acceptDeclineInviteView: AcceptDeclineInviteView,
private val directLogoutView: DirectLogoutView,
private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
) : Node(buildContext, plugins = plugins) {
init {
lifecycle.subscribe(
@ -88,6 +93,7 @@ class RoomListNode @AssistedInject constructor(
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = LocalContext.current as Activity
RoomListView(
state = state,
onRoomClick = this::onRoomClick,
@ -98,6 +104,13 @@ class RoomListNode @AssistedInject constructor(
onRoomSettingsClick = this::onRoomSettingsClick,
onMenuActionClick = { onMenuActionClick(activity, it) },
onRoomDirectorySearchClick = this::onRoomDirectorySearchClick,
onMigrateToNativeSlidingSyncClick = {
if (state.directLogoutState.canDoDirectSignOut) {
state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
} else {
plugins<RoomListEntryPoint.Callback>().forEach { it.onLogoutForNativeSlidingSyncMigrationNeeded() }
}
},
modifier = modifier,
) {
acceptDeclineInviteView.Render(
@ -107,5 +120,9 @@ class RoomListNode @AssistedInject constructor(
modifier = Modifier
)
}
directLogoutView.Render(state.directLogoutState) {
enableNativeSlidingSyncUseCase()
}
}
}

View file

@ -29,6 +29,7 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
@ -88,6 +89,7 @@ class RoomListPresenter @Inject constructor(
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
private val fullScreenIntentPermissionsPresenter: FullScreenIntentPermissionsPresenter,
private val notificationCleaner: NotificationCleaner,
private val logoutPresenter: DirectLogoutPresenter,
) : Presenter<RoomListState> {
private val encryptionService: EncryptionService = client.encryptionService()
private val syncService: SyncService = client.syncService()
@ -115,13 +117,15 @@ class RoomListPresenter @Inject constructor(
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
val directLogoutState = logoutPresenter.present()
fun handleEvents(event: RoomListEvents) {
when (event) {
is RoomListEvents.UpdateVisibleRange -> coroutineScope.launch {
updateVisibleRange(event.range)
}
RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
RoomListEvents.DismissRecoveryKeyPrompt -> securityBannerDismissed = true
RoomListEvents.DismissBanner -> securityBannerDismissed = true
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
is RoomListEvents.ShowContextMenu -> {
coroutineScope.showContextMenu(event, contextMenu)
@ -161,6 +165,7 @@ class RoomListPresenter @Inject constructor(
searchState = searchState,
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
directLogoutState = directLogoutState,
eventSink = ::handleEvents,
)
}
@ -168,6 +173,7 @@ class RoomListPresenter @Inject constructor(
@Composable
private fun securityBannerState(
securityBannerDismissed: Boolean,
needsSlidingSyncMigration: Boolean,
): State<SecurityBannerState> {
val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed)
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
@ -185,6 +191,7 @@ class RoomListPresenter @Inject constructor(
RecoveryState.ENABLED -> SecurityBannerState.None
}
}
needsSlidingSyncMigration -> SecurityBannerState.NeedsNativeSlidingSyncMigration
else -> SecurityBannerState.None
}
}
@ -209,11 +216,14 @@ class RoomListPresenter @Inject constructor(
loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading
}
}
val needsSlidingSyncMigration by produceState(false) {
value = client.isNativeSlidingSyncSupported() && !client.isUsingNativeSlidingSync()
}
return when {
showEmpty -> RoomListContentState.Empty
showSkeleton -> RoomListContentState.Skeleton(count = 16)
else -> {
val securityBannerState by securityBannerState(securityBannerDismissed)
val securityBannerState by securityBannerState(securityBannerDismissed, needsSlidingSyncMigration)
RoomListContentState.Rooms(
securityBannerState = securityBannerState,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),

View file

@ -10,6 +10,7 @@ package io.element.android.features.roomlist.impl
import androidx.compose.runtime.Immutable
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchState
@ -31,6 +32,7 @@ data class RoomListState(
val searchState: RoomListSearchState,
val contentState: RoomListContentState,
val acceptDeclineInviteState: AcceptDeclineInviteState,
val directLogoutState: DirectLogoutState,
val eventSink: (RoomListEvents) -> Unit,
) {
val displayFilters = contentState is RoomListContentState.Rooms
@ -59,6 +61,7 @@ enum class SecurityBannerState {
None,
SetUpRecovery,
RecoveryKeyConfirmation,
NeedsNativeSlidingSyncMigration,
}
@Immutable

View file

@ -12,6 +12,8 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.api.aLeaveRoomState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
@ -57,6 +59,7 @@ internal fun aRoomListState(
filtersState: RoomListFiltersState = aRoomListFiltersState(),
contentState: RoomListContentState = aRoomsContentState(),
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
directLogoutState: DirectLogoutState = aDirectLogoutState(),
eventSink: (RoomListEvents) -> Unit = {}
) = RoomListState(
matrixUser = matrixUser,
@ -69,6 +72,7 @@ internal fun aRoomListState(
searchState = searchState,
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
directLogoutState = directLogoutState,
eventSink = eventSink,
)

View file

@ -50,6 +50,7 @@ fun RoomListView(
onRoomSettingsClick: (roomId: RoomId) -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
onRoomDirectorySearchClick: () -> Unit,
onMigrateToNativeSlidingSyncClick: () -> Unit,
modifier: Modifier = Modifier,
acceptDeclineInviteView: @Composable () -> Unit,
) {
@ -76,6 +77,7 @@ fun RoomListView(
onOpenSettings = onSettingsClick,
onCreateRoomClick = onCreateRoomClick,
onMenuActionClick = onMenuActionClick,
onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
modifier = Modifier.padding(top = topPadding),
)
// This overlaid view will only be visible when state.displaySearchResults is true
@ -105,6 +107,7 @@ private fun RoomListScaffold(
onOpenSettings: () -> Unit,
onCreateRoomClick: () -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
onMigrateToNativeSlidingSyncClick: () -> Unit,
modifier: Modifier = Modifier,
) {
fun onRoomClick(room: RoomListRoomSummary) {
@ -140,6 +143,7 @@ private fun RoomListScaffold(
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = ::onRoomClick,
onCreateRoomClick = onCreateRoomClick,
onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
@ -180,5 +184,6 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class)
onMenuActionClick = {},
onRoomDirectorySearchClick = {},
acceptDeclineInviteView = {},
onMigrateToNativeSlidingSyncClick = {},
)
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@Composable
internal fun NativeSlidingSyncMigrationBanner(
onContinueClick: () -> Unit,
onDismissClick: () -> Unit,
modifier: Modifier = Modifier,
) {
DialogLikeBannerMolecule(
modifier = modifier,
title = stringResource(R.string.banner_migrate_to_native_sliding_sync_title),
content = stringResource(R.string.banner_migrate_to_native_sliding_sync_description),
actionText = stringResource(R.string.banner_migrate_to_native_sliding_sync_action),
onSubmitClick = onContinueClick,
onDismissClick = onDismissClick,
)
}
@PreviewsDayNight
@Composable
internal fun NativeSlidingSyncMigrationBannerPreview() = ElementPreview {
NativeSlidingSyncMigrationBanner(
onContinueClick = {},
onDismissClick = {},
)
}

View file

@ -64,6 +64,7 @@ fun RoomListContentView(
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
onCreateRoomClick: () -> Unit,
onMigrateToNativeSlidingSyncClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
@ -85,6 +86,7 @@ fun RoomListContentView(
eventSink = eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
onRoomClick = onRoomClick,
)
}
@ -133,6 +135,7 @@ private fun RoomsView(
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
onMigrateToNativeSlidingSyncClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
@ -147,6 +150,7 @@ private fun RoomsView(
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = onRoomClick,
onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
modifier = modifier.fillMaxSize(),
)
}
@ -159,6 +163,7 @@ private fun RoomsViewList(
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
onMigrateToNativeSlidingSyncClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val lazyListState = rememberLazyListState()
@ -185,7 +190,7 @@ private fun RoomsViewList(
item {
SetUpRecoveryKeyBanner(
onContinueClick = onSetUpRecoveryClick,
onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }
)
}
}
@ -193,7 +198,15 @@ private fun RoomsViewList(
item {
ConfirmRecoveryKeyBanner(
onContinueClick = onConfirmRecoveryKeyClick,
onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }
)
}
}
SecurityBannerState.NeedsNativeSlidingSyncMigration -> {
item {
NativeSlidingSyncMigrationBanner(
onContinueClick = onMigrateToNativeSlidingSyncClick,
onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }
)
}
}
@ -278,5 +291,6 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
onConfirmRecoveryKeyClick = {},
onRoomClick = {},
onCreateRoomClick = {},
onMigrateToNativeSlidingSyncClick = {},
)
}

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Log Out &amp; Upgrade"</string>
<string name="banner_migrate_to_native_sliding_sync_description">"Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."</string>
<string name="banner_migrate_to_native_sliding_sync_title">"Upgrade available"</string>
<string name="banner_set_up_recovery_content">"Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."</string>
<string name="banner_set_up_recovery_title">"Set up recovery"</string>
<string name="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."</string>

View file

@ -7,6 +7,7 @@
package io.element.android.features.roomlist.impl
import androidx.compose.runtime.Composable
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
@ -18,6 +19,8 @@ import io.element.android.features.invite.api.response.anAcceptDeclineInviteStat
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
@ -240,7 +243,7 @@ class RoomListPresenterTest {
sessionVerificationService = FakeSessionVerificationService().apply {
givenNeedsSessionVerification(false)
},
syncService = FakeSyncService(MutableStateFlow(SyncState.Running))
syncService = FakeSyncService(MutableStateFlow(SyncState.Running)),
)
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
val presenter = createRoomListPresenter(
@ -268,7 +271,7 @@ class RoomListPresenterTest {
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SetUpRecovery)
nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt)
nextState.eventSink(RoomListEvents.DismissBanner)
val finalState = awaitItem()
assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
scope.cancel()
@ -644,6 +647,10 @@ class RoomListPresenterTest {
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
logoutPresenter: DirectLogoutPresenter = object : DirectLogoutPresenter {
@Composable
override fun present() = aDirectLogoutState()
},
) = RoomListPresenter(
client = client,
networkMonitor = networkMonitor,
@ -671,5 +678,6 @@ class RoomListPresenterTest {
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
fullScreenIntentPermissionsPresenter = FakeFullScreenIntentPermissionsPresenter(),
notificationCleaner = notificationCleaner,
logoutPresenter = logoutPresenter,
)
}

View file

@ -68,7 +68,7 @@ class RoomListViewTest {
val close = rule.activity.getString(CommonStrings.action_close)
rule.onNodeWithContentDescription(close).performClick()
eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt)
eventsRecorder.assertSingle(RoomListEvents.DismissBanner)
}
@Test
@ -86,7 +86,7 @@ class RoomListViewTest {
val close = rule.activity.getString(CommonStrings.action_close)
rule.onNodeWithContentDescription(close).performClick()
eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt)
eventsRecorder.assertSingle(RoomListEvents.DismissBanner)
}
@Test
@ -232,6 +232,21 @@ class RoomListViewTest {
listOf(RoomListEvents.AcceptInvite(invitedRoom), RoomListEvents.DeclineInvite(invitedRoom)),
)
}
@Test
fun `clicking on logout and migrate calls the migration clicked callback`() {
val state = aRoomListState(
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.NeedsNativeSlidingSyncMigration),
eventSink = {},
)
ensureCalledOnce { callback ->
rule.setRoomListView(
state = state,
onMigrateToNativeSlidingSyncClick = callback,
)
rule.clickOn(R.string.banner_migrate_to_native_sliding_sync_action)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomListView(
@ -244,6 +259,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onMenuActionClick: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(),
onMigrateToNativeSlidingSyncClick: () -> Unit = EnsureNeverCalled()
) {
setContent {
RoomListView(
@ -256,6 +272,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
onRoomSettingsClick = onRoomSettingsClick,
onMenuActionClick = onMenuActionClick,
onRoomDirectorySearchClick = onRoomDirectorySearchClick,
onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
acceptDeclineInviteView = { },
)
}