diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index db153314d6..5a8db185ef 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -40,6 +40,7 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueState +import io.element.android.features.logout.api.LogoutEntryPoint import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.preferences.api.PreferencesEntryPoint @@ -65,6 +66,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.sync.SyncState +import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview @@ -96,6 +98,8 @@ class LoggedInFlowNode @AssistedInject constructor( private val shareEntryPoint: ShareEntryPoint, private val matrixClient: MatrixClient, private val sendingQueue: SendQueues, + private val logoutEntryPoint: LogoutEntryPoint, + private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase, snackbarDispatcher: SnackbarDispatcher, ) : BaseFlowNode( backstack = BackStack( @@ -225,6 +229,9 @@ class LoggedInFlowNode @AssistedInject constructor( @Parcelize data class IncomingShare(val intent: Intent) : NavTarget + + @Parcelize + data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -271,6 +278,10 @@ class LoggedInFlowNode @AssistedInject constructor( override fun onRoomDirectorySearchClick() { backstack.push(NavTarget.RoomDirectorySearch) } + + override fun onLogoutForNativeSlidingSyncMigrationNeeded() { + backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded) + } } roomListEntryPoint .nodeBuilder(this, buildContext) @@ -407,6 +418,20 @@ class LoggedInFlowNode @AssistedInject constructor( .params(ShareEntryPoint.Params(intent = navTarget.intent)) .build() } + is NavTarget.LogoutForNativeSlidingSyncMigrationNeeded -> { + val callback = object : LogoutEntryPoint.Callback { + override fun onChangeRecoveryKeyClick() { + backstack.push(NavTarget.SecureBackup()) + } + } + + logoutEntryPoint.nodeBuilder(this, buildContext) + .onSuccessfulLogoutPendingAction { + enableNativeSlidingSyncUseCase() + } + .callback(callback) + .build() + } } } diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt index d90f039ec1..ea952b0100 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt @@ -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 } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt index 7c7264a00f..08de15c2ee 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt @@ -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(buildContext, plugins) } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt index fb2c1585a0..dd9788cd1d 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt @@ -33,6 +33,12 @@ class LogoutNode @AssistedInject constructor( plugins().forEach { it.onChangeRecoveryKeyClick() } } + interface SuccessfulLogoutPendingAction : Plugin { + fun onSuccessfulLogoutPendingAction() + } + + private val customOnSuccessfulLogoutPendingAction = plugins().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, ) diff --git a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt index 7070fbe915..98ed717f7b 100644 --- a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt +++ b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt @@ -29,5 +29,6 @@ interface RoomListEntryPoint : FeatureEntryPoint { fun onRoomSettingsClick(roomId: RoomId) fun onReportBugClick() fun onRoomDirectorySearchClick() + fun onLogoutForNativeSlidingSyncMigrationNeeded() } } diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index 5e3dc513c6..ce747859f5 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -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) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt index 4e586056fd..6656ca8231 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt @@ -20,6 +20,7 @@ open class RoomListContentStateProvider : PreviewParameterProvider().forEach { it.onLogoutForNativeSlidingSyncMigrationNeeded() } + } + }, modifier = modifier, ) { acceptDeclineInviteView.Render( @@ -107,5 +120,9 @@ class RoomListNode @AssistedInject constructor( modifier = Modifier ) } + + directLogoutView.Render(state.directLogoutState) { + enableNativeSlidingSyncUseCase() + } } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index d2b45c0d06..2983b6b695 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -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, private val fullScreenIntentPermissionsPresenter: FullScreenIntentPermissionsPresenter, private val notificationCleaner: NotificationCleaner, + private val logoutPresenter: DirectLogoutPresenter, ) : Presenter { 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.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 { 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(), diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index bb95b85293..ae9a721bfa 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -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 diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 31499600ba..b9d003e32c 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -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, ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index cfa5605c1e..4db0f68e4e 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -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 = {}, ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt new file mode 100644 index 0000000000..6d058d6a8a --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt @@ -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 = {}, + ) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt index 8abbce3068..d4af5a3fe5 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt @@ -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 = {}, ) } diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index 6affb2bb16..ff6c626c63 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -1,5 +1,8 @@ + "Log Out & Upgrade" + "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." + "Upgrade available" "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices." "Set up recovery" "Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup." diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt index 1b639d2b19..8b7294b165 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt @@ -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 = Presenter { aRoomListSearchState() }, acceptDeclineInvitePresenter: Presenter = 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, ) } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt index 1c218c9aaa..63327d0d49 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt @@ -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 AndroidComposeTestRule.setRoomListView( @@ -244,6 +259,7 @@ private fun AndroidComposeTestRule.setRoomL onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onMenuActionClick: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(), onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(), + onMigrateToNativeSlidingSyncClick: () -> Unit = EnsureNeverCalled() ) { setContent { RoomListView( @@ -256,6 +272,7 @@ private fun AndroidComposeTestRule.setRoomL onRoomSettingsClick = onRoomSettingsClick, onMenuActionClick = onMenuActionClick, onRoomDirectorySearchClick = onRoomDirectorySearchClick, + onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick, acceptDeclineInviteView = { }, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt index b9fd8d7fe0..966ce5b0b9 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt @@ -39,6 +39,7 @@ fun DialogLikeBannerMolecule( onSubmitClick: () -> Unit, onDismissClick: (() -> Unit)?, modifier: Modifier = Modifier, + actionText: String = stringResource(CommonStrings.action_continue), ) { Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { Surface( @@ -74,7 +75,7 @@ fun DialogLikeBannerMolecule( ) Spacer(modifier = Modifier.height(12.dp)) Button( - text = stringResource(CommonStrings.action_continue), + text = actionText, size = ButtonSize.Medium, modifier = Modifier.fillMaxWidth(), onClick = onSubmitClick, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 65dfa39c27..2591a5cfd3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -126,4 +126,10 @@ interface MatrixClient : Closeable { */ suspend fun getUrl(url: String): Result suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result + + /** Returns `true` if the home server supports native sliding sync. */ + suspend fun isNativeSlidingSyncSupported(): Boolean + + /** Returns `true` if the current session is using native sliding sync. */ + fun isUsingNativeSlidingSync(): Boolean } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index b7fb6d1566..86c14c2e54 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -93,6 +93,7 @@ import org.matrix.rustcomponents.sdk.IgnoredUsersListener import org.matrix.rustcomponents.sdk.NotificationProcessSetup import org.matrix.rustcomponents.sdk.PowerLevels import org.matrix.rustcomponents.sdk.SendQueueRoomErrorListener +import org.matrix.rustcomponents.sdk.SlidingSyncVersion import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.use import timber.log.Timber @@ -528,6 +529,14 @@ class RustMatrixClient( }) }.buffer(Channel.UNLIMITED) + override suspend fun isNativeSlidingSyncSupported(): Boolean { + return client.availableSlidingSyncVersions().contains(SlidingSyncVersion.Native) + } + + override fun isUsingNativeSlidingSync(): Boolean { + return client.session().slidingSyncVersion == SlidingSyncVersion.Native + } + internal fun setDelegate(delegate: RustClientSessionDelegate) { client.setDelegate(delegate) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 040f24fb95..f4085bfb51 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -77,6 +77,8 @@ class FakeMatrixClient( private val clearCacheLambda: () -> Unit = { lambdaError() }, private val userIdServerNameLambda: () -> String = { lambdaError() }, private val getUrlLambda: (String) -> Result = { lambdaError() }, + var isNativeSlidingSyncSupportedLambda: suspend () -> Boolean = { true }, + var isUsingNativeSlidingSyncLambda: () -> Boolean = { true }, ) : MatrixClient { var setDisplayNameCalled: Boolean = false private set @@ -316,4 +318,12 @@ class FakeMatrixClient( override suspend fun getUrl(url: String): Result { return getUrlLambda(url) } + + override suspend fun isNativeSlidingSyncSupported(): Boolean { + return isNativeSlidingSyncSupportedLambda() + } + + override fun isUsingNativeSlidingSync(): Boolean { + return isUsingNativeSlidingSyncLambda() + } } diff --git a/libraries/preferences/api/build.gradle.kts b/libraries/preferences/api/build.gradle.kts index a8a1566987..b9ab6630df 100644 --- a/libraries/preferences/api/build.gradle.kts +++ b/libraries/preferences/api/build.gradle.kts @@ -17,4 +17,8 @@ dependencies { implementation(libs.coroutines.core) implementation(projects.libraries.matrix.api) implementation(libs.androidx.datastore.preferences) + + testImplementation(projects.libraries.preferences.test) + testImplementation(libs.test.truth) + testImplementation(libs.coroutines.test) } diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt new file mode 100644 index 0000000000..e5bcbad18e --- /dev/null +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt @@ -0,0 +1,23 @@ +/* + * 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.libraries.preferences.api.store + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class EnableNativeSlidingSyncUseCase @Inject constructor( + private val appPreferencesStore: AppPreferencesStore, + private val appCoroutineScope: CoroutineScope, +) { + operator fun invoke() { + appCoroutineScope.launch { + appPreferencesStore.setSimplifiedSlidingSyncEnabled(true) + } + } +} diff --git a/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt b/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt new file mode 100644 index 0000000000..315c8f91d6 --- /dev/null +++ b/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt @@ -0,0 +1,31 @@ +/* + * 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.libraries.preferences.api.store + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class EnableNativeSlidingSyncUseCaseTest { + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `ensure that the use case sets the simplified sliding sync enabled flag`() = runTest { + val preferencesStore = InMemoryAppPreferencesStore() + val useCase = EnableNativeSlidingSyncUseCase(preferencesStore, this) + assertThat(preferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse() + + useCase() + advanceUntilIdle() + + assertThat(preferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue() + } +} diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 40a930d7e6..ae7b6d58d4 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(projects.features.roomlist.impl) implementation(projects.features.leaveroom.impl) implementation(projects.features.login.impl) + implementation(projects.features.logout.impl) implementation(projects.features.networkmonitor.impl) implementation(projects.services.toolbox.impl) implementation(projects.libraries.featureflag.impl) diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index a3516ba43f..70f2a1f8a4 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter import io.element.android.features.invite.impl.response.AcceptDeclineInviteView import io.element.android.features.leaveroom.impl.DefaultLeaveRoomPresenter +import io.element.android.features.logout.impl.direct.DefaultDirectLogoutPresenter import io.element.android.features.networkmonitor.impl.DefaultNetworkMonitor import io.element.android.features.roomlist.impl.RoomListPresenter import io.element.android.features.roomlist.impl.RoomListView @@ -144,6 +145,7 @@ class RoomListScreen( } }, notificationCleaner = FakeNotificationCleaner(), + logoutPresenter = DefaultDirectLogoutPresenter(matrixClient, encryptionService), ) @Composable @@ -172,7 +174,8 @@ class RoomListScreen( modifier = modifier, acceptDeclineInviteView = { AcceptDeclineInviteView(state = state.acceptDeclineInviteState, onAcceptInvite = {}, onDeclineInvite = {}) - } + }, + onMigrateToNativeSlidingSyncClick = {}, ) DisposableEffect(Unit) { diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png new file mode 100644 index 0000000000..d5c79327d5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45f8b8767d1a54afde878f1c853a7fd6dd5823d04db3b75a7c18c5d6fc742d8e +size 36110 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png new file mode 100644 index 0000000000..f875fdea79 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6849a6c73341edc3f37fd6ef968c07e63277d18f833028386012fa2a438ade7e +size 34946 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png new file mode 100644 index 0000000000..dfcee5c2f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4ee87f2ae823b3d026b70d6b91463ba6aa3ad7e454b915c0eba3901b219169b +size 72188 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png new file mode 100644 index 0000000000..ff6ac65155 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:863e7f66501f357728aa048560dc59fccd63bff8fc4b648296d82db119b0137d +size 71031 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index c6a256b1bd..f736825725 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -139,6 +139,7 @@ "session_verification_banner_.*", "confirm_recovery_key_banner_.*", "banner\\.set_up_recovery\\..*", + "banner\\.migrate_to_native_sliding_sync\\..*", "full_screen_intent_banner_.*", "screen_migration_.*", "screen_invites_.*"